Cleartext listener never speaks HTTP/2: gRPC peering impossible over plain HTTP (h2c missing) #143

Closed
opened 2026-08-09 01:03:52 +00:00 by Cordy · 0 comments
Owner

Second live find of the #106 dogfood, immediately behind #140. With the dial port fixed, the next hop fails:

peering: hello: rpc error: code = Unavailable desc = connection error:
desc = "error reading server preface: http2: failed reading the frame payload:
http2: frame too large, note that the frame header looked like an HTTP/1.1 header"

Root cause: the client dials plain-HTTP peers with prior-knowledge HTTP/2 (h2c), but cairnd's listener is a stock http.Server.ListenAndServe() — HTTP/1.1 only on cleartext. The server answers the h2 preface with an HTTP/1.1 error response, which the client reports as the "frame header looked like an HTTP/1.1 header" mismatch. peering.Mux's isGRPC check (ProtoMajor == 2) is correct but unreachable on cleartext: no request ever arrives as HTTP/2.

Why tests missed it: the loopback test wires the gRPC server via an injected dialer/in-process listener; the single-port mux was never exercised through a real cleartext TCP connection.

Fix: wrap the root handler in h2c.NewHandler(root, &http2.Server{}) (golang.org/x/net/http2/h2c, already in the module graph as an indirect). Ordinary HTTP/1.1 traffic passes through untouched; TLS deployments keep HTTP/2 via ALPN exactly as before; the LAN plain-HTTP case — which isPrivateHost deliberately permits — becomes real. Add the missing test: an httptest server with h2c + peering.Mux, one prior-knowledge HTTP/2 request with a gRPC content type routed to the gRPC handler, one HTTP/1.1 request routed to the fallback.

Second live find of the #106 dogfood, immediately behind #140. With the dial port fixed, the next hop fails: ``` peering: hello: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: http2: failed reading the frame payload: http2: frame too large, note that the frame header looked like an HTTP/1.1 header" ``` **Root cause:** the client dials plain-HTTP peers with prior-knowledge HTTP/2 (h2c), but `cairnd`'s listener is a stock `http.Server.ListenAndServe()` — HTTP/1.1 only on cleartext. The server answers the h2 preface with an HTTP/1.1 error response, which the client reports as the "frame header looked like an HTTP/1.1 header" mismatch. `peering.Mux`'s `isGRPC` check (`ProtoMajor == 2`) is correct but unreachable on cleartext: no request ever arrives as HTTP/2. **Why tests missed it:** the loopback test wires the gRPC server via an injected dialer/in-process listener; the single-port mux was never exercised through a real cleartext TCP connection. **Fix:** wrap the root handler in `h2c.NewHandler(root, &http2.Server{})` (golang.org/x/net/http2/h2c, already in the module graph as an indirect). Ordinary HTTP/1.1 traffic passes through untouched; TLS deployments keep HTTP/2 via ALPN exactly as before; the LAN plain-HTTP case — which `isPrivateHost` deliberately permits — becomes real. Add the missing test: an httptest server with h2c + `peering.Mux`, one prior-knowledge HTTP/2 request with a gRPC content type routed to the gRPC handler, one HTTP/1.1 request routed to the fallback.
Cordy closed this issue 2026-08-09 01:06:35 +00:00
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#143
No description provided.