Repository navigation
Add a request argument to write_body so HTTP/1.0 request bodies of unknown length raise instead of being close-delimited. - #65
Open
samuel-williams-shopify wants to merge 3 commits into
Conversation
samuel-williams-shopify
force-pushed
the
http10-request-body-length
branch
from
November 5, 2026 01:22
7e7663f to
b5d5265
Compare
request argument to write_body so HTTP/1.0 request bodies of unknown length raise instead of being close-delimited.
ioquatix
force-pushed
the
http10-request-body-length
branch
from
November 5, 2026 09:54
b5d5265 to
5505bf8
Compare
…f unknown length raise instead of being close-delimited.
samuel-williams-shopify
force-pushed
the
http10-request-body-length
branch
from
November 5, 2026 10:04
5505bf8 to
f02eae0
Compare
This branch has not been deployed
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.
Protocol::HTTP1::Connection#write_bodytakes a trailingrequestargument; when true, an HTTP/1.0 body of unknown length raisesProtocolErrorinstead of being written close-delimited. HTTP/1.0 responses continue to be delimited by closing the connection, as before.Problem
A request body cannot be close-delimited. Servers (RFC 1945 §7.2.2, RFC 9112 §6.3; verified against Go
net/httpand protocol-http1's own server) read zero body bytes for an HTTP/1.0 request withoutcontent-lengthortransfer-encoding, and either discard the remaining bytes or, if the connection is kept alive, parse them as the next request.write_bodycurrently has one fall-through for an HTTP/1.0 body that is non-empty with nolength:write_body_and_close. That framing is only meaningful for a response. When a client calls it for a request (asasync-http's HTTP/1 client does for any unknown-length body, since HTTP/1.0 has no chunked encoding), the body bytes are written where the server expects the next request.Verification
Against a Go
net/httpserver:POST /b HTTP/1.0withConnection: keep-aliveand a body ofGET /smuggled HTTP/1.1\r\nHost: localhost\r\n\r\n, followed by a half-close. Go served/bwith an empty body and then served/smuggled. Withoutkeep-alive, the body bytes were discarded. The same happens with protocol-http1's own server: the new test shows theGET /otherbytes would otherwise reach the wire, and the companion async-http test shows the application being invoked for both/and/smuggledonmain.Solution
write_body(version, body, head = false, trailer = nil, request = false). The new argument mirrorshead: the caller already knows whether it is writing a request or a response, so the framing decision is made from that rather than from connection state. In the HTTP/1.0 unknown-length branch:requesttrue: the body is closed with aProtocolError(Cannot write #{version} request body of unknown length!) which is then raised, before any body bytes are written. HTTP/1.0 clients must buffer such bodies to obtain a length.@persistent = falseandwrite_body_and_close.After the raise, the connection is still
:open: the request head has been written without the terminating blank line. The@raisesdocumentation states that the caller must either write a body with a known length or close the connection, and that callingread_responsewithout doing so will deadlock. The test pins this by asserting the client is:open?before closing it, and that only the head reached the server.The
Traces::Providerwrapper forwrite_bodyaccepts and records the new argument. The parameter is last with a default, so existing positional callers are unaffected.Downstream
socketry/async-http#260 passes
truefromProtocol::HTTP1::Clientand bumps itsprotocol-http1constraint to~> 0.42, so it must not merge before this is released. Because that client writes the body in a child task which closes the connection on failure, the error currently surfaces to users asCannot read response in state: closed!rather than this message; surfacing the original error is a separate follow-up there.Tests
In
test/protocol/http1/connection.rb:with "#write_body":"can write closed body"(response, default) still dispatches towrite_body_and_close;"refuses to write closed body for a request"asserts thatrequest = trueraises, closes the body, leaves the connection open and writes nothing;"can write chunked body for a request"asserts HTTP/1.1 withrequest = truestill uses chunked encoding.with "HTTP/1.0 request body", driven through a real client/server pair withrequest = true: a known-length body is read back as a fixed body with the connection persistent; an empty unknown-length body still producescontent-length: 0; an unknown-length body raises, closes the body, leaves the client:open?, and the server's stream contains exactly the request head and none of theGET /otherbytes."transitions to the closed state when using connection: close response body"continues to cover the HTTP/1.0 close-delimited response path end to end.