You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
client: don't synthesize the default port into the Host header (#1318)
* client: don't synthesize the default port into the Host header
For a URL without an explicit port, the client built the request `Host`
header from the connection address, which always carries the scheme's
default port (`example.com:443`). The Host header should mirror the URL
authority as written, so a bare-host URL must send a bare `Host`.
Sending `Host: host:443` is legal per RFC 9110 but breaks any server that
treats the Host verbatim. Concretely it breaks AWS SigV4 presigned URLs:
the signature is computed over the canonical host (`s3.amazonaws.com`), so
a request whose Host carries `:443` is rejected with SignatureDoesNotMatch
(403). Go's net/http, curl and HTTP.jl 1.x all keep the Host as written.
Add a `host_header` view on the parsed URL that preserves an explicit port
but never synthesizes the default one (keeping IPv6 brackets), and build
`request.host` from it. The connection address is unchanged, so dialing
still targets the right port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* client: route streaming, WebSocket and redirect paths through host_header
The previous commit stopped synthesizing the scheme's default port into the
Host header, but only for the high-level request path. The sibling client
paths still built the request from the connection address, so a default-port
URL leaked Host: host:443 (and broke AWS SigV4) on:
- HTTP.open / streaming requests (built from parsed.address)
- HTTP and WebSocket redirects (reset Host to current_address per hop)
- WebSocket handshakes (initial host=parsed.address)
Wire all of them through the authority-as-written value:
- Streaming and WebSocket init now use parsed.host_header.
- _resolve_redirect_target also returns the next hop's host_header (parsed
for a host-changing hop, current host carried through for a relative hop),
and both redirect loops reset request.host from it instead of the address.
The dial address is unchanged everywhere, so connections still target the
right port. Matches Go's net/http, verified empirically: Go sends the URL
authority as written on the initial request and derives the Host from the
next URL on a redirect, never synthesizing the default port.
Adds focused regressions for the streaming constructor and the redirect
resolver (including explicit-default-port preservation), plus a WebSocket
handshake Host assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add WebSocket handshake Host regression for authority-as-written
The streaming and redirect paths got focused host_header regressions, but
the WebSocket coverage only asserted the explicit-port branch. Exercise the
ws->http / wss->https mapping in `_parse_websocket_url` directly: a
default-port wss/ws URL must yield a bare `Host`, while an explicit or custom
port is preserved and the dial address keeps its port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* client: accept a nothing Host when resolving a redirect target
The previous commit threaded `current_request.host` into the new
`current_host_header::String` parameter of `_resolve_redirect_target`. But
`Request.host` is `Union{Nothing,String}` and is `nothing` for low-level
`do!` callers that pin `Host` only in the request headers, so a redirect threw
a MethodError instead of being followed.
Loosen the parameter to `Union{Nothing,String}`. On a relative redirect with
no parsed host, fall back to the dial `current_address` (carrying the port, as
before this PR) instead of propagating `nothing` — `_prepare_request_for_redirect`
strips the `Host` header each hop, so returning `nothing` would drop the
`Host` header from the redirected request entirely. Absolute redirects keep
returning the parsed `host_header`.
Adds a unit case for the `nothing` current host and an end-to-end regression:
a `do!` request with `Host` only in headers (`request.host === nothing`)
following a relative redirect now completes and the redirected hop still
carries a valid `Host`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 commit comments