Skip to content

fix(std): restore the h2c Upgrade handshake the stdlib migration dropped - #576

Merged
FumingPower3925 merged 1 commit into
mainfrom
fix/std-h2c-upgrade-regression
Sep 11, 2026
Merged

fix(std): restore the h2c Upgrade handshake the stdlib migration dropped#576
FumingPower3925 merged 1 commit into
mainfrom
fix/std-h2c-upgrade-regression

Conversation

@FumingPower3925

Copy link
Copy Markdown
Contributor

Reopens the substance of #440 and fixes the regression it caused. Found by the cluster nightly, not by anything in this repo.

What broke

#440 moved the std engine off x/net/http2/h2c and onto http.Server.Protocols + SetUnencryptedHTTP2. That covers prior-knowledge h2c. It does not cover the RFC 7540 §3.2 HTTP/1.1 Upgrade handshake.

net/http implements the upgrade path internally — serveHTTP2Conn takes an UpgradeRequest and Settings — but exposes no way for a caller to reach it, and the registration hook it does expose is reserved for x/net/http2 itself. So the "stdlib replacement" is not a replacement for this half. The migration removed a capability rather than relocating it.

epoll and io_uring implement the handshake themselves via Config.EnableH2Upgrade and were untouched. Three engines, two behaviours, on a project whose release bar is that they agree.

How it got through

My commit message on #440 said:

Nothing depended on it: the spec suite's upgrade tests already skip std, and the benchmark harness only ever advertises H2CUpgrade for the io_uring column.

Both clauses are true. The conclusion was wrong, and the reason is worth recording. test/spec/h2c_upgrade_test.go does skip std — and its comment says why:

if se.name == "std" {
    // std engine uses x/net/http2/h2c middleware; its upgrade path is
    // separate. These tests target the custom engines.

The coverage had been delegated to the middleware. Removing the middleware removed the coverage with it, and a skip that reads as "covered elsewhere" looks identical to a skip that reads as "not applicable". I checked the two places that skip this engine and not the one that exercises it.

That place is the cluster nightly, which drove 3,597 upgrade preambles at kitchen_sink/std on both architectures and got back not one 101:

kitchen_sink  std  amd64  tier_1.h2c_churn.h2c_upgraded  0
   vacuous h2c slice: 3597 upgrade preambles, not one 101 -- h2c_hang/h2c_crashed judged nothing
kitchen_sink  std  arm64  tier_1.h2c_churn.h2c_upgraded  0

That slice is not incidental coverage. It exists to drive teardown races against the 101 path — RST before the 101, RST after it, a truncated client preface — and it is how #470 was found. On std it had been testing nothing.

The fix

Restores h2c.NewHandler, which serves both shapes (client preface and upgrade) and falls through to the wrapped handler for plain HTTP/1.1.

The deprecation stands, and this is a deliberate choice: a deprecated package that does the job beats a supported one that silently does less. The stale nolint text from before #440 does not come back — the old one claimed "We pin Go 1.26.3 (see go.mod)" while go.mod said 1.27.0, which was the true defect #440 found. The new comment states the actual reason, which is that no stdlib equivalent exists.

The test that was missing

engine/std/h2c_upgrade_test.go asserts the 101 on the engine that owns the behaviour, rather than on the engines that were already covered. It also asserts an H2C listener still answers plain HTTP/1.1 — the half most likely to break while fixing the first.

Negative control, reinstating exactly what #440 shipped:

--- FAIL: TestStdEngineAnswersH2CUpgradeWith101
    h2c upgrade got "HTTP/1.1 200 OK", want a 101 Switching Protocols

The server ignores the Upgrade header and serves the request normally, which is precisely what the cluster saw.

engine/std and test/conformance pass.

Worth deciding separately

Whether celeris should support h2c Upgrade at all is a real question — RFC 9113 removed it and Go dropped it deliberately. But that decision should be made on purpose, for all three engines at once, not fall out of a dependency migration. This PR restores the status quo so it can be.

celeris#440 moved this engine off x/net/http2/h2c and onto
http.Server.Protocols + SetUnencryptedHTTP2. That covers prior-knowledge
h2c. It does NOT cover the RFC 7540 3.2 HTTP/1.1 Upgrade handshake:
net/http implements the upgrade path internally (serveHTTP2Conn takes an
UpgradeRequest and Settings) but exposes no way for a caller to reach it,
and the hook it does expose is reserved for x/net/http2 itself. So the
migration silently removed a capability rather than relocating it.

epoll and io_uring implement the handshake themselves via
Config.EnableH2Upgrade and were untouched. Three engines, two behaviours,
on a project whose release bar is that they agree.

My commit message on #440 said "nothing depended on it: the spec suite's
upgrade tests already skip std, and the benchmark harness only ever
advertises H2CUpgrade for the io_uring column". Both clauses are true and
the conclusion was wrong. test/spec/h2c_upgrade_test.go skips std, and its
comment says why -- "std engine uses x/net/http2/h2c middleware; its
upgrade path is separate" -- so the coverage had been DELEGATED to the
middleware. Removing the middleware removed the coverage with it. I checked
the two places that skip this engine and not the one that exercises it: the
cluster nightly, which drove 3,597 upgrade preambles at kitchen_sink/std on
both arches and got back not one 101.

Restores h2c.NewHandler, which serves both shapes -- client preface and
upgrade -- and falls through to the wrapped handler for plain HTTP/1.1. The
deprecation stands; a deprecated package that does the job beats a
supported one that silently does less. The stale nolint text from before
#440 ("We pin Go 1.26.3") does not come back: the new comment says the
actual reason, which is that no stdlib equivalent exists.

Adds the case that was missing. engine/std/h2c_upgrade_test.go asserts the
101 on the engine that owns the behaviour, plus that an H2C listener still
answers plain HTTP/1.1 -- the half most likely to break while fixing the
first. Reinstating exactly what #440 shipped fails it:

    h2c upgrade got "HTTP/1.1 200 OK", want a 101 Switching Protocols

engine/std and test/conformance pass.
@FumingPower3925
FumingPower3925 merged commit 1f3b43f into main Sep 11, 2026
10 checks passed
@FumingPower3925
FumingPower3925 deleted the fix/std-h2c-upgrade-regression branch September 11, 2026 14:13
FumingPower3925 added a commit to goceleris/probatorium that referenced this pull request Sep 11, 2026
…ench server (#324)

Picks up the h2c Upgrade regression fix (goceleris/celeris#576). The nightly
on the previous pin failed on exactly that: kitchen_sink/std drove 3,597
upgrade preambles on both arches and got back not one 101, because #440's
stdlib migration silently dropped the RFC 7540 3.2 handshake from the std
engine while epoll and io_uring kept it.

Property side of that run was already clean -- 71,680 evaluations, zero
violations -- so this repin is about re-running the h2c slice against an
engine that can actually answer it.
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.

1 participant