Skip to content

http2: shared h2c negotiation layer - #3706

Merged
benoitc merged 3 commits into
masterfrom
feat/h2c
Aug 21, 2026
Merged

http2: shared h2c negotiation layer#3706
benoitc merged 3 commits into
masterfrom
feat/h2c

Conversation

@benoitc

@benoitc benoitc commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Builds on #3663 by @fluffy-dev, whose two commits are kept as they are. Closes
part of #3489.

The negotiation logic moves to a shared module so the three workers decide the
same way. Only gthread is wired up here; gevent and ASGI come next, and they read
their bytes very differently, which is exactly why the decision has to live in
one place and not be written three times.

Two things change from #3663.

The preface timeout was applied to each read instead of to the preface as a
whole. A client sending one byte at a time stayed under it forever and kept a
thread pool slot busy, which is what the slow-client protection in #3518 exists
to prevent. It is now a deadline for the whole preface.

http2_prior_knowledge becomes http2_cleartext, which takes off,
prior-knowledge, upgrade or both. Upgrade: h2c is coming later, and
turning one mechanism on must not turn the other on. A boolean cannot say that.

Breaking changes

None for anyone: the setting it renames was never released.

--http2-prior-knowledge becomes --http2-cleartext prior-knowledge. In a
config file, http2_cleartext = True is still understood and means
prior-knowledge, so the boolean form does not break silently.

fluffy-dev and others added 3 commits August 21, 2026 00:35
…eface with 400

Address review feedback on the h2c fallback path:

- Rename --h2c to --http2-prior-knowledge: the flag only ever meant the
  RFC 9113 prior-knowledge form, so the name now says exactly that.
- Only sniff the connection preface when the peer is trusted per
  forwarded_allow_ips (same list, same semantics as forwarded headers,
  unix socket peers included). Untrusted peers are served HTTP/1.1
  exactly as if the flag were off - no sniff, no preface timeout.
- A trusted peer that sends anything other than a valid preface (an
  HTTP/1.x request, a malformed or stalled preface) now gets an
  unconditional 400 via the new InvalidH2CPreface error instead of a
  silent HTTP/1.1 fallback. On a prior-knowledge port the trusted proxy
  is expected to speak HTTP/2; downgrading would only hide
  misconfiguration and invite odd interactions with keepalive and
  worker timeouts.

The 101-upgrade dance remains deliberately unimplemented.

Tests updated: trusted-peer rejection paths, untrusted-peer passthrough,
wildcard allow list, unix socket peer. Full suite passes.
Moves preface matching, the trust check and the blocking preface read
into gunicorn/http2/negotiation.py so gthread, gevent and the ASGI
worker decide the same way. Only gthread is wired up here.

The preface timeout becomes a budget for the whole preface rather than
for each read, so a client sending one byte at a time cannot hold a
connection, and a thread pool slot, for as long as the preface is long.

http2_prior_knowledge becomes http2_cleartext, taking off,
prior-knowledge, upgrade or both, because upgrade is coming and enabling
one mechanism must not enable the other.
@benoitc
benoitc merged commit d684eee into master Aug 21, 2026
26 checks passed
@benoitc
benoitc deleted the feat/h2c branch August 21, 2026 06:25
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.

2 participants