Fix Host header for HTTP/2 upstream requests (Gitea self-check mismatch)

HTTP/2 requests carry the client's host only in the :authority
pseudo-header, with no literal Host header. build_upstream_request
copied headers verbatim, so the upstream request had no Host, and
hyper-util's legacy client (set_host=true default) filled it in from
the upstream URI, sending Host: 127.0.0.1:3000 instead of the client's
host. Backends like Gitea build absolute URLs from that host, causing
the admin self-check mismatch and intermittent wrong asset URLs.

Reconstruct Host from the request URI authority for HTTP/2, preserve
the client's Host header for HTTP/1.1, and let the URI authority win
if both are present.
This commit is contained in:
glm-5.3-flash committed 2026-09-12 05:38:52 +00:00
1 parent 71ad3c2905
commit 203fcadffd
3 files changed
+243 -3

No files matched your search

+3 -2
View File
@@ -139,7 +139,7 @@ existing `X-Forwarded-For` headers from the client cannot be trusted.
| Header | Value Source | Notes |
|--------|-------------|-------|
| `Host` | Original request `Host` header | Preserved as-is |
| `Host` | Client's host: the literal `Host` header for HTTP/1.1, or the `:authority` pseudo-header (request URI authority) for HTTP/2 | Reconstructed for HTTP/2, which carries no literal `Host` header (RFC 9113 §8.3.1). Sent as `127.0.0.1:3000` otherwise, breaking backends that build absolute URLs from it (e.g. Gitea). If both URI authority and `Host` header are present, the authority wins. |
| `X-Real-IP` | `ConnectInfo<SocketAddr>` remote IP | Set to client's IP address |
| `X-Forwarded-For` | `ConnectInfo<SocketAddr>` remote IP | **Replaced**, not appended. The proxy is the edge proxy — there are no trusted proxies upstream, so existing `X-Forwarded-For` values from the client cannot be trusted. |
| `X-Forwarded-Proto` | Determined by which listener port received the request | `https` for requests on the listener's `https_port`, `http` for requests on the listener's `http_port`. Note: since the TLS-terminating listener only receives HTTPS connections, this is always `"https"` in practice. The HTTP redirect listener sends a 301 redirect rather than proxying, so `X-Forwarded-Proto` is not set there. See OQ-11. |
@@ -159,7 +159,8 @@ The proxy handler constructs a new request to the upstream:
return 502 Bad Gateway and log the error at `warn` level. The proxy must
never silently drop parts of the URI (such as the query string) — a
malformed upstream URI is an error, not a recoverable condition.
2. Copy the request method, headers, and body from the original
2. Copy the request method, headers, and body from the original, normalizing
the `Host` header to the client's host (see the table above)
3. Inject proxy headers (X-Real-IP, X-Forwarded-For, X-Forwarded-Proto)
4. Remove hop-by-hop headers (Connection, Keep-Alive, Transfer-Encoding, etc.)
5. Send the request via a shared hyper Client instance