Skip to content

Let on_headers_complete veto an upgrade and keep the body - #149

Open
afonsojanu wants to merge 1 commit into
MagicStack:masterfrom
afonsojanu:fix/veto-upgrade-preserves-body
Open

Let on_headers_complete veto an upgrade and keep the body#149
afonsojanu wants to merge 1 commit into
MagicStack:masterfrom
afonsojanu:fix/veto-upgrade-preserves-body

Conversation

@afonsojanu

Copy link
Copy Markdown

Fixes #124.

Right now cb_on_headers_complete pauses parsing (raising HttpParserUpgrade) the moment a request carries an Upgrade header, no matter what the embedding application actually wants to do about it. That's a real problem for something like Upgrade: h2c, which a lot of clients send speculatively on plain HTTP/1.1 requests and which most servers have no intention of acting on. Since the pause fires before any body gets parsed, a request that also has a body (via Content-Length or Transfer-Encoding: chunked) loses that body entirely - RFC 7230 6.7 explicitly allows a server to just ignore the upgrade and keep processing the message normally, but there was no way to tell the parser to do that.

I traced this by reproducing the exact scenario from the issue (POST with Upgrade: h2c + chunked body): feed_data() raises HttpParserUpgrade right after headers, and even if you catch it and feed the leftover bytes back in, the parser is already expecting a fresh request line at that point, not a body continuation - so there's no way to recover the body from outside.

The fix is small: on_headers_complete's return value used to be discarded. Now, returning False from it tells cb_on_headers_complete to skip the pause and let llhttp keep parsing the message as normal (following whatever Content-Length/Transfer-Encoding says). HttpParserUpgrade still gets raised once the message is fully done, so the application still finds out the client asked for an upgrade - it's just no longer forced to lose the body to learn that. Anything that doesn't explicitly return False (the existing behavior for every current caller) is unaffected.

Added two tests: one confirming the veto path parses the body correctly and still eventually raises HttpParserUpgrade, and one confirming an upgrade that isn't vetoed (the existing websocket fixture) still pauses immediately with no body read, exactly like before. Full suite is green (43 passed).

llhttp pauses parsing right at headers-complete whenever a request
carries an Upgrade header, regardless of whether the application
actually intends to switch protocols. There was no way for a Python
protocol to say "ignore this one, keep parsing as an ordinary
HTTP/1.1 message" - so a request declaring Upgrade: h2c (which many
clients send speculatively and most servers never act on) got its
body silently dropped, because llhttp's own pause fires before the
body is ever read.

on_headers_complete's return value was previously discarded. Now
returning False from it tells the parser to skip the pause and
proceed to parse the body normally, matching what RFC 7230 6.7 says
a server is allowed to do when it declines an upgrade. The parser
still raises HttpParserUpgrade once the message ends, so the
application still learns the client asked for one - vetoing the
pause doesn't erase that it happened, it just stops losing the body
in the meantime. Anything that doesn't return False keeps behaving
exactly as before.
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.

[Bug] Request body lost when Upgrade: h2c + Transfer-Encoding: chunked is used

1 participant