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
_guess_version decides the HTTP version by trial-parsing: try httpv1, and if that declines, try httpv2. That answers "did a parser accept this?" when the question is "what is this?" — and it gets both directions wrong. Measured on main:
real 24-octet HTTP/2 preface -> answers version=2, but by misparsing b'PRI' as a frame
header (Length 0x505249 = 5263433), with a ProtocolWarning
b'foo bar baz\r\nX: y\r\n\r\n' -> answers version=2 <- garbage text classified as HTTP/2
It reaches the right answer on the preface for the wrong reason, and the wrong answer on text that is not HTTP at all. The maintainer's requirement, verbatim from #682:
We guess on the HTTP version and definitely have to make sure the guesing is correct and making sense.
Expected behavior
A positive identification before any parse attempt: compare the first 24 octets against b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'. RFC 9113 §3.4 designed that sequence for exactly this purpose — it is deliberately a well-formed HTTP/1.1 request line whose method PRI is reserved and unregistered, so an HTTP/1 parser rejects it and an HTTP/2 detector recognises it. A prefix compare cannot false-positive on valid HTTP/1 and needs no parse.
Then fall through to the existing HTTP/1 start-line regexes, which are correctly anchored (httpv1.py:60, :62, :72), and only then to a parse attempt.
What this does not fix, and should not pretend to
The Upgrade: h2c form (RFC 7540 §3.2, deprecated but not removed by RFC 9113 §3.1) is stateful and not expressible in this shape at all. On the wire the upgrade request is HTTP/1.1 and parses correctly today; the switch takes effect after the 101, so deciding that later segments on the same 4-tuple are HTTP/2 needs per-connection state. _guess_version receives one payload with no flow context. Recognising Upgrade: h2c in a request is possible; acting on it is not.
Likewise a mid-stream segment — a bare HTTP/2 frame header with no preface, or an opaque HTTP/1 body chunk — is genuinely undecidable from one payload. The honest outcome there is Raw, not a coin-flip. Any heuristic on the 9-byte frame header ("type ≤ 9, reserved bit clear") will misfire on binary HTTP/1 bodies, which is precisely how b'foo bar baz…' is classified HTTP/2 today.
Additional context
Sequencing — this is step 2 of 3, from the maintainer's ruling on #682 ("TCP:80 should use the proxy I think, since both HTTP/1 and HTTP/2 bind on them"):
The order matters and is not cosmetic. _guess_version is entered 0 times across all 1604 frames of the fixture corpus today, because every HTTP frame arrives over TCP and tcp.py:330 binds httpv1 directly. Repointing first would put 231 real HTTP/1 frames through a guess path that is known-wrong on non-HTTP input, trading an honest Raw for a confident wrong HTTP/2 on the main traffic path.
Also note the corpus cannot test this. No fixture uses UDP 80/8080 and none carries HTTP/2, so a preface test needs its own synthetic coverage — a real preface, a preface plus SETTINGS, garbage text, a mid-stream frame header, and an Upgrade: h2c exchange asserted to stay HTTP/1.1.
Describe the bug
_guess_versiondecides the HTTP version by trial-parsing: tryhttpv1, and if that declines, tryhttpv2. That answers "did a parser accept this?" when the question is "what is this?" — and it gets both directions wrong. Measured onmain:It reaches the right answer on the preface for the wrong reason, and the wrong answer on text that is not HTTP at all. The maintainer's requirement, verbatim from #682:
Expected behavior
A positive identification before any parse attempt: compare the first 24 octets against
b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'. RFC 9113 §3.4 designed that sequence for exactly this purpose — it is deliberately a well-formed HTTP/1.1 request line whose methodPRIis reserved and unregistered, so an HTTP/1 parser rejects it and an HTTP/2 detector recognises it. A prefix compare cannot false-positive on valid HTTP/1 and needs no parse.Then fall through to the existing HTTP/1 start-line regexes, which are correctly anchored (
httpv1.py:60,:62,:72), and only then to a parse attempt.What this does not fix, and should not pretend to
The
Upgrade: h2cform (RFC 7540 §3.2, deprecated but not removed by RFC 9113 §3.1) is stateful and not expressible in this shape at all. On the wire the upgrade request is HTTP/1.1 and parses correctly today; the switch takes effect after the101, so deciding that later segments on the same 4-tuple are HTTP/2 needs per-connection state._guess_versionreceives one payload with no flow context. RecognisingUpgrade: h2cin a request is possible; acting on it is not.Likewise a mid-stream segment — a bare HTTP/2 frame header with no preface, or an opaque HTTP/1 body chunk — is genuinely undecidable from one payload. The honest outcome there is
Raw, not a coin-flip. Any heuristic on the 9-byte frame header ("type ≤ 9, reserved bit clear") will misfire on binary HTTP/1 bodies, which is precisely howb'foo bar baz…'is classified HTTP/2 today.Additional context
Sequencing — this is step 2 of 3, from the maintainer's ruling on #682 ("TCP:80 should use the proxy I think, since both HTTP/1 and HTTP/2 bind on them"):
httpv2's guard tests the declared length, not the buffer, so the sub-9 class behaves inconsistently. In flight.httpv1to the proxy.The order matters and is not cosmetic.
_guess_versionis entered 0 times across all 1604 frames of the fixture corpus today, because every HTTP frame arrives over TCP andtcp.py:330bindshttpv1directly. Repointing first would put 231 real HTTP/1 frames through a guess path that is known-wrong on non-HTTP input, trading an honestRawfor a confident wrongHTTP/2on the main traffic path.Also note the corpus cannot test this. No fixture uses UDP 80/8080 and none carries HTTP/2, so a preface test needs its own synthetic coverage — a real preface, a preface plus SETTINGS, garbage text, a mid-stream frame header, and an
Upgrade: h2cexchange asserted to stay HTTP/1.1.Related: #682, #799, #787, #789.