Skip to content

feat(writer): configurable default host for relative includes - #17

Merged
darkweak merged 1 commit into
masterfrom
feat/default-host
Nov 10, 2026
Merged

darkweak merged 1 commit into
masterfrom
feat/default-host

Conversation

@darkweak

Copy link
Copy Markdown
Owner

Closes #14

Problem

A relative include (<esi:include src="/my-esi-content/whatever"/>) is always resolved against the host of the incoming request. When one proxy (e.g. Traefik in Kubernetes) routes several apps, the fragments live on an internal service, so the host had to be hardcoded in the HTML and the app rebuilt per environment.

Change

  • writer: new ParseBaseURL(string) and WithBaseURL(*url.URL) option. NewWriter(rw, rq, opts ...Option) is variadic, so existing callers are unaffected.
    • Accepts host[:port] (scheme defaults to http) or http(s)://host[:port]. Rejects paths, queries, userinfo and other schemes.
    • The include request takes the base scheme and host but keeps the incoming path, so src="fragment" still resolves relative to the page. The incoming request is not modified, and $(HTTP_HOST) still reports the original host.
    • Absolute includes are unchanged.
  • Traefik: defaultHost plugin setting:
    spec:
      plugin:
        esi:
          defaultHost: static-webserver.my-app.svc.cluster.local
  • Caddy: esi { default_host backend:8080 }
  • RoadRunner: http.esi.default_host
  • README updated.

Notes

  • Cookie/Authorization are forwarded to the default host, since it becomes the same-origin target for relative includes (the same as before, when they were forwarded to the request host). Absolute includes pointing at the public host no longer count as same-origin when a default host is set.
  • The issue's "full" service + namespace form isn't included: a plugin can't resolve Kubernetes references, and service.namespace.svc.cluster.local[:port] does the same thing.

Testing

  • Unit tests for ParseBaseURL and WithBaseURL (include routing, relative path, cookie forwarding, original request unchanged), plus one integration test each for the Traefik, Caddy (caddytest) and RoadRunner middlewares.
  • go test -race ./... passes in the root module and all three middleware modules, and golangci-lint reports no new issues.
  • Checked by hand in traefik:latest (Yaegi) with defaultHost: whoami: the relative include went to the whoami service with Host: whoami.

🤖 Generated with Claude Code

Relative include URLs were always resolved against the incoming request
host, so a single proxy routing several apps (e.g. Traefik in Kubernetes)
had to hardcode the fragment host in the HTML and rebuild per environment.

- writer: add ParseBaseURL and the WithBaseURL option (NewWriter is now
  variadic, so existing callers keep compiling). The include request gets
  the base scheme/host while keeping the incoming path; the incoming
  request is not modified.
- traefik: `defaultHost` plugin setting.
- caddy: `default_host` subdirective (`esi { default_host ... }`).
- roadrunner: `http.esi.default_host` setting.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@darkweak
darkweak merged commit 179a390 into master Nov 10, 2026
3 of 4 checks passed
@darkweak
darkweak deleted the feat/default-host branch November 10, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Define optional Host for relative URL paths

1 participant