Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublicTeam
Assigneesstack72

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

  1. swamp serve --host [IP-1] --port 9443 --cert-file … --key-file … --auth-mode token --admins user:kneel
  2. Front it with a proxy offering ALPN h2,http/1.1 on :443 that reverse_proxyes WebSocket to serve (e.g. Caddy).
  3. SWAMP_SERVER_TOKEN=[REDACTED-SECRET-1] swamp workflow run <wf> --server wss://<proxied-host>Error: "Could not connect to wss://<host>/".
  4. Raw TLS+WS probe isolating ALPN:
    • Offer [h2, http/1.1] → proxy negotiates h2 → WS Upgrade hangs/timeouts.
    • Offer [http/1.1] → negotiates http/1.1 → server returns HTTP/1.1 101 Switching Protocols.
    • Plain curl/curl -6 to the same host → 200 (TLS, cert, routing all fine — the failure is ALPN-specific).

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).

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 3 MOREFINDINGS+ 3 MOREDEVIATIONS_JUSTIFIED+ 1 MORECONTRIBUTOR_NOTIFIED

Shipped

7/23/2026, 10:41:39 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
kneel moved this from the private collective Elowish to public9/1/2026, 2:59:22 AM
stack72 assigned stack727/22/2026, 7:00:39 PM
Editable. Press Enter to edit.

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.1 only → negotiates fine.
  • stream 1 — ClientHello ALPN offers h2 only → 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 Upgrade over h2 → invalid → hangs → Could not connect.
  • h2 disabled server-side (Caddy protocols h1 h3): stream 1 offers only h2, server has no common ALPN → fatal TLS alert 120 no_application_protocolCould 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 Protocols
  • Authorization: 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.

stack72 commented 7/23/2026, 10:41:48 PM

Thanks @kneel for reporting this! The fix has been merged and a release is on its way. We appreciate your contribution to swamp.

Sign in to post a ripple.