Skip to content

Spike: middleware/websocket over HTTP/2 (RFC 8441) #252

@FumingPower3925

Description

@FumingPower3925

Context

Today middleware/websocket returns 426 Upgrade Required for any HTTP/2 client request. RFC 8441 (2018) defines how to speak WebSocket over HTTP/2 by using the CONNECT method with a :protocol pseudo-header. Adoption is patchy but present in modern infra (Envoy, HAProxy H2 upstream mode, curl, and some browsers in specific contexts), and it's the only real spec-compliance gap in the WebSocket middleware.

This is a spike, not a straight-forward enhancement, because RFC 8441 requires changes in the H2 processor (Extended CONNECT negotiation, :protocol pseudo-header acceptance, stream-scoped raw-frame routing) that don't exist today. Realistically sized size/L — may need to slide to v1.5.0 or v1.6.0 once sized more carefully.

What RFC 8441 requires

1. Server advertises support via a SETTINGS frame

SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) = 1

Sent during the initial H2 handshake. Clients that don't see this SETTING must fall back to H1 Upgrade: websocket.

2. Server accepts Extended CONNECT

A normal H2 request has pseudo-headers :method, :scheme, :authority, :path. An Extended CONNECT adds :protocol and uses :method = CONNECT:

:method    = CONNECT
:scheme    = https
:authority = example.com
:path      = /ws
:protocol  = websocket

Plus normal WS negotiation headers (Sec-WebSocket-Version: 13, Sec-WebSocket-Protocol, Sec-WebSocket-Extensions). Note: Sec-WebSocket-Key is omitted over H2 — the handshake is inherent to the stream, there's no HTTP/1-style 101 exchange.

3. Server acknowledges with :status = 200 on that stream

No 101 Switching Protocols — H2 has no such mechanism. The stream then carries WebSocket frames (RFC 6455 framing) as the DATA frame payloads. Each H2 DATA frame carries one or more WS frames; WS fragmentation interacts with H2 flow control in non-obvious ways.

4. Stream lifetime = WebSocket lifetime

When either side sends :status 200 (server) or the first DATA frame, the WS session is live. WS close frame → server sends END_STREAM to finish the H2 stream. A client RST_STREAM translates to a WS abort.

Design considerations

  1. H2 processor changes. protocol/h2/stream/processor.go currently rejects :method = CONNECT and any pseudo-header other than the standard four. Needs:

    • Advertise SETTINGS_ENABLE_CONNECT_PROTOCOL=1 when a WS route is registered.
    • Accept :protocol = websocket on CONNECT streams.
    • Route these streams to a new dispatch path that hands the raw DATA-frame byte stream to the middleware/websocket Handler, exactly like H1 hijack does today.
  2. Detach model. H1 WS detaches the whole TCP conn. H2 WS detaches a single stream — the connection still multiplexes other streams in parallel. Celeris' Context.Detach today is conn-scoped; we'd need a stream-scoped variant or a new middleware/websocket/h2 path that talks directly to the H2 processor without invoking Detach.

  3. Flow-control interaction. WS's own fragmentation + H2 WINDOW_UPDATE frames need careful coordination. A slow WS reader can backpressure the stream (correct) but a bug here can starve the whole connection (other streams' flow control). middleware/websocket's existing backpressure hooks map cleanly to stream-level WINDOW_UPDATE; validate the math.

  4. Compression (permessage-deflate). The H2 CONTINUATION story interacts with permessage-deflate's sliding window. Keep it simple: reject permessage-deflate negotiation on WS-over-H2 in the first version (clients will drop back to uncompressed), ship compression support in a follow-up.

  5. Autobahn. fuzzingclient doesn't currently test RFC 8441. Need a separate validation path — probably a Go-side integration test using golang.org/x/net/http2 client that speaks Extended CONNECT.

Scope of spike

  • Prototype the H2 processor changes (SETTINGS_ENABLE_CONNECT_PROTOCOL, Extended CONNECT acceptance, stream-scoped DATA routing).
  • Prototype a stream-scoped WebSocket adapter that reuses as much of middleware/websocket's Conn as possible.
  • Interop: run against golang.org/x/net/http2 client speaking Extended CONNECT; decide if browser testing is in scope (most browsers don't expose WS-over-H2 to JS yet — Envoy/HAProxy are the real interop targets).
  • Size the full implementation and decide whether it ships in v1.4.2 or slides to v1.5.0 / v1.6.0.

Exit criteria

  1. Decision doc committed at middleware/websocket/H2-DESIGN.md describing the chosen detach/dispatch model.
  2. End-to-end test: x/net/http2 client completes Extended CONNECT to a celeris server, exchanges echo frames, closes cleanly.
  3. Memory + latency characterization vs the H1 WS path: H2 WS should be within 2× of H1 WS latency (the extra framing is real, but the multiplex win offsets it at multi-stream concurrency).
  4. Autobahn-analog: a Go-side conformance suite (~30 cases) covering the subset of WS behaviors RFC 8441 changes (handshake, framing over DATA, close → END_STREAM, RST_STREAM abort).
  5. Decision: size/L realistic? If spike reveals size/XL, move to v1.5.0 / v1.6.0 and scope the spike down.

Critical files (expected touch points)

  • protocol/h2/stream/processor.go — Extended CONNECT acceptance, SETTINGS advertisement.
  • internal/conn/h2.go — server-preface SETTINGS emission when any WS route is registered.
  • middleware/websocket/upgrade.go — detect H2 + Extended CONNECT, dispatch to H2 adapter.
  • New: middleware/websocket/h2.go — stream-scoped Conn wrapper.
  • test/conformance/ws_h2_test.go (new) — x/net/http2-driven interop tests.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/engineEngine interface or implementationarea/protocolProtocol parsing / detectionenhancementNew feature or requestprotocol/h2HTTP/2 protocolsize/L~3-5 days of work

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions