Skip to content

http: _guess_version trial-parses instead of identifying, so it answers HTTP/2 for garbage text and for the preface by accident #800

Description

@JarryShaw

Describe the bug

_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"):

  1. httpv2: the frame guard tests the declared length, not the buffer, so a 4-octet frame can report length=16777215 #799 — httpv2's guard tests the declared length, not the buffer, so the sub-9 class behaves inconsistently. In flight.
  2. This issue — positive preface identification, so the guessing is correct rather than merely reachable.
  3. register_protocol's key space is still not unique: the three HTTP classes share one key (follow-up to #675) #682 — repoint TCP:80/8080 from httpv1 to the proxy.

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.

Related: #682, #799, #787, #789.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugfixPull requests that fix a defect (fix: subject prefix)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions