fix(std): restore the h2c Upgrade handshake the stdlib migration dropped - #576
Merged
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/h2cand ontohttp.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 —
serveHTTP2Conntakes anUpgradeRequestandSettings— but exposes no way for a caller to reach it, and the registration hook it does expose is reserved forx/net/http2itself. So the "stdlib replacement" is not a replacement for this half. The migration removed a capability rather than relocating it.epollandio_uringimplement the handshake themselves viaConfig.EnableH2Upgradeand 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:
Both clauses are true. The conclusion was wrong, and the reason is worth recording.
test/spec/h2c_upgrade_test.godoes skip std — and its comment says why: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: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.goasserts 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:
The server ignores the Upgrade header and serves the request normally, which is precisely what the cluster saw.
engine/stdandtest/conformancepass.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.