Relationships
#1345 --server wss:// client offers h2 ALPN then fails the WebSocket upgrade over HTTP/2 (should pin http/1.1)
Opened by kneel · 7/22/2026· Shipped 7/23/2026
Description
The --server wss://… client fails to connect when the serve endpoint is fronted by a reverse proxy that negotiates HTTP/2. The client offers ALPN h2, http/1.1; the proxy selects h2; the client then sends an HTTP/1.1-style WebSocket Upgrade over the h2 connection — which is invalid (WS-over-HTTP/2 requires RFC 8441 Extended CONNECT). The upgrade hangs and the client reports the opaque Error: "Could not connect to wss://…/". When only http/1.1 is offered, the upgrade returns 101 Switching Protocols and works end-to-end.
Impact
Any swamp serve reached through an HTTP/2-capable reverse proxy (Caddy/nginx) — a common production setup — is unreachable via --server, with no actionable error. --log-level trace -v did not surface the underlying cause (a secondary diagnosability bug).
Steps to reproduce
swamp serve --host [IP-1] --port 9443 --cert-file … --key-file … --auth-mode token --admins user:kneel- Front it with a proxy offering ALPN
h2,http/1.1on :443 thatreverse_proxyes WebSocket to serve (e.g. Caddy). SWAMP_SERVER_TOKEN=[REDACTED-SECRET-1] swamp workflow run <wf> --server wss://<proxied-host>→Error: "Could not connect to wss://<host>/".- Raw TLS+WS probe isolating ALPN:
- Offer
[h2, http/1.1]→ proxy negotiates h2 → WSUpgradehangs/timeouts. - Offer
[http/1.1]→ negotiates http/1.1 → server returnsHTTP/1.1 101 Switching Protocols. - Plain
curl/curl -6to the same host →200(TLS, cert, routing all fine — the failure is ALPN-specific).
- Offer
Expected
A WebSocket client (RFC 6455) should offer ALPN http/1.1 only for wss://, or implement RFC 8441 for WS-over-h2. Pinning http/1.1 in the client's TLS ALPN fixes it.
Secondary
Surface the real handshake failure (negotiated ALPN / HTTP status / TLS result) instead of the generic "Could not connect" — even at --log-level trace.
Environment
swamp 20260720.142822.0-sha.0caa5556, Linux (client + server same build), --auth-mode token, behind OPNsense os-caddy (Caddy 2, HTTP/2 on :443).
Shipped
Click a lifecycle step above to view its details.
kneel commented 7/22/2026, 8:27:47 PM
Reproduced end-to-end with packet captures — and a caveat: disabling h2 server-side does NOT work around it
Client 20260720.142822.0-sha.0caa5556, --server wss://… through a Caddy reverse proxy in front of a swamp serve (TLS, --auth-mode token).
tshark on the client connection shows two TLS connections per run:
- stream 0 — ClientHello ALPN offers
http/1.1only → negotiates fine. - stream 1 — ClientHello ALPN offers
h2only → this is the channel that carries the run, and it's the one that fails.
So it isn't only "offers h2,http/1.1 then upgrades wrong" — the client opens a dedicated h2-only TLS connection for its main channel. Consequences:
- h2 available (Caddy default): stream 1 negotiates h2, client attempts an HTTP/1.1-style
Upgradeover h2 → invalid → hangs →Could not connect. - h2 disabled server-side (Caddy
protocols h1 h3): stream 1 offers onlyh2, server has no common ALPN → fatal TLS alert120 no_application_protocol→Could not connect(~2.3s). So the obvious mitigation (turn off HTTP/2 at the proxy) makes it worse, not better, because the client won't fall back to http/1.1 on that connection.
The server side is fine. With the token supplied, swamp serve's WS endpoint completes the handshake over http/1.1 — verified with curl both through the proxy and direct to the serve:
…/?token=[REDACTED-SECRET-1](Connection: Upgrade) →101 Switching ProtocolsAuthorization: Bearer <name.secret>→ upgrade completes
Reproduces direct to the serve too (serve offers h2 ALPN), so it's not proxy-specific.
Fix direction: for the WS/run channel, either don't offer h2 in the ALPN list (pin http/1.1, per the title), or implement WS-over-HTTP/2 per RFC 8441 (Extended CONNECT). A server-side ALPN toggle can't rescue it as long as the client opens an h2-only connection.
Sign in to post a ripple.