Write Host as the first HTTP/1.1 request header field - #1366
Merged
Merged
Conversation
RFC 9112 §3.2 recommends that a user agent send Host as the first field line after the request line, and curl, Go and Python all do. The HTTP/1 writer injected Host from request.host with setheader, which appends a key that is not already stored, so Host landed after every client-default and caller-supplied header. Some CDN front ends answer 403 to such a request and 200 to the otherwise identical request with Host first. _prepare_request_headers_for_write now hoists Host to the front of the header copy it writes: exactly one Host line, taking the caller's non-empty Host header value when present and request.host otherwise. A caller-supplied Host is moved rather than duplicated, and Request.headers itself is not reordered. Every HTTP/1 request writer (write_request!, the transport's proxy-plan writer, the CONNECT tunnel request and the WebSocket handshake) shares this preparation. Fixes #1361 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1366 +/- ##
==========================================
+ Coverage 89.51% 89.60% +0.08%
==========================================
Files 31 31
Lines 12594 12635 +41
==========================================
+ Hits 11274 11321 +47
+ Misses 1320 1314 -6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The HTTP/1.1 client wrote the
Hostheader after every other header:RFC 9112 §3.2 says a user agent that sends
HostSHOULD send it as the first field line after the request line, and curl, Go'snet/httpand Python'shttp.clientall do. This is not only cosmetic: as reported in #1361, a CDN-fronted HTTPS site answered 403 whenHostcame last and 200 for the otherwise identical request withHostfirst.Root cause:
_prepare_request_headers_for_writeinsrc/http1.jlinjectedHostfromrequest.hostwithsetheader, which appends a key that is not already stored. By the time the request is written, the high-level client has already storedUser-Agent,Accept-Encodingand the caller's headers, soHostalways landed after them. A caller-suppliedHostheader simply kept whatever position it had in the caller's list.Change
src/http1.jl: new_hoist_host_header!replaces thesetheaderinjection. It writes exactly oneHostline as the first header field: the caller'sHostheader value when one is stored with a non-empty value, otherwiserequest.host. A caller-suppliedHostis moved to the front (not duplicated) and any further storedHostentries are dropped; an empty-valued callerHostis kept as-is when there is norequest.host(RFC 9112 §3.2.2 requires an emptyHostfor authority-less targets); when there is noHostanywhere none is invented, as before. The hoist happens on the copy that_prepare_request_headers_for_writealready makes, soRequest.headersis not reordered.write_request!, the transport's proxy-plan writer (absolute-form forward-proxy requests, withProxy-Authorizationfollowing the caller's headers), theCONNECTtunnel request, and the WebSocket handshake. The HTTP/2 client uses:authorityand is unaffected.write_request!docstring andCHANGELOG.mdupdated.Tests
test/http1_wire_tests.jl: new testset asserting the exact wire bytes forHostderived fromrequest.host(the reporter's request shape), a callerHoststored after other headers moved to the front with the caller's stored order left alone, a lower-case caller key, duplicate storedHostentries collapsing to one, empty callerHostwith and withoutrequest.host, noHostat all on HTTP/1.0, the forward-proxy absolute-form path via both thewrite_request!keywords and_write_request_head!with anHTTP_FORWARDplan, and theCONNECTtunnel request as built by the transport (which still round-trips throughread_request).test/http_client_tests.jl: end-to-end testset throughHTTP.requestagainst a raw TCP listener that captures the request head:Hostis the second line (right after the request line) and appears exactly once, both when derived from the URL and when supplied by the caller inheaders.Pkg.teston Julia 1.12.6: passed.Fixes #1361
🤖 Generated with Claude Code