v1.3.14 — WebSocket upgrades work on HTTPS upstreams¶
Critical fix. Pre-v1.3.14 reverse_proxy did not advertise HTTP/1.1 explicitly to HTTPS upstreams; Caddy's ALPN negotiation chose HTTP/2 by default and the classic RFC 6455 WebSocket upgrade had no compatible connection to ride. Result: UniFi Network Control Plane (and every other SPA whose backend speaks HTTPS + WebSockets) loaded blank.
Bug¶
Reproducing flow on UniFi:
| Path | Pre-v1.3.14 | Browser symptom |
|---|---|---|
GET / | HTTP/2 200 | shell loads |
GET /api/ws/system (WS upgrade) | HTTP/2 500 | dashboard never populates |
GET /api/ws/webrtc/local (WS upgrade) | HTTP/2 500 | live console blank |
The pattern repeats on any HTTPS-upstream backend whose realtime layer is a WebSocket: Home Assistant (when configured HTTPS), Jellyfin streaming, n8n editor, Vaultwarden Send. HTTP upstreams (the more common shape in the test stack) were unaffected -- Caddy never tried HTTP/2 there.
Root cause¶
backend/internal/caddycfg/caddycfg.go > reverseProxyFromTG emitted, for HTTPS:
...with no versions field. For HTTP upstreams the transport was omitted entirely (Caddy default).
When versions is absent and TLS is set, Caddy lets Go's http.Transport pick from whatever ALPN negotiates. With most modern HTTPS backends advertising both H1 and H2, the transport prefers H2. WS upgrade requires Connection: upgrade semantics that don't translate cleanly onto an H2 multiplexed stream.
Fix¶
reverseProxyFromTG now always emits the transport block with explicit versions: ["1.1", "2"]:
- if tg.Protocol == models.ProtocolHTTPS {
- t := &transport{Protocol: "http", TLS: &transportTLS{}}
- if !tg.VerifyTLS {
- t.TLS.InsecureSkipVerify = true
- }
- rp.Transport = t
- }
+ t := &transport{
+ Protocol: "http",
+ Versions: []string{"1.1", "2"},
+ }
+ if tg.Protocol == models.ProtocolHTTPS {
+ t.TLS = &transportTLS{}
+ if !tg.VerifyTLS {
+ t.TLS.InsecureSkipVerify = true
+ }
+ }
+ rp.Transport = t
["1.1", "2"] (HTTP/1.1 first) means:
- WebSocket upgrade hits HTTP/1.1 immediately. RFC 6455 semantics work. Backends with HTTPS upstream that previously 500'd now 101 cleanly.
- Non-WS HTTP traffic still gets HTTP/2 when the upstream advertises it via ALPN. Go's transport per-request dispatches to the right protocol.
- Plain HTTP upstreams: the
"2"entry is a no-op (Go'shttp.Transportdoes not do h2c without TLS), so plaintext backends behave identically. The transport block now exists but has no functional effect on those routes.
Verification¶
Live config dump, post-fix:
$ docker exec argos-prod-caddy wget -qO- http://localhost:2019/config/ \
| python3 -c '...' | grep -A 5 transport
transport: {
"protocol": "http",
"versions": ["1.1", "2"]
}
Manual WS handshake against an argos-fronted Home Assistant (HTTP upstream, used as a no-regression baseline):
$ curl -i -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
http://iot.example.com/api/websocket
HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: websocket
Server: Caddy
Server: Python/3.14 aiohttp/3.13.5
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Plain HTTP traffic on the same stack (regression check):
The HTTPS-upstream UniFi case lives outside this test stack and will be confirmed by the operator. The unit tests in transport_test.go lock in the JSON shape against future regression.
Tests (3 new, all passing)¶
TestTransportEmitsVersionsForHTTPSUpstream-- HTTPS upstream emitstransport.protocol="http",versions: ["1.1", ...], and preserves the TLS sub-block.TestTransportEmitsVersionsForHTTPUpstream-- HTTP upstream emits the transport block withversionsstarting at1.1and crucially NOtlssub-block (would silently break a non-TLS backend during ALPN).TestTransportInsecureSkipVerifyHonoured--verify_tls=falseproducesinsecure_skip_verify=trueso self-signed backends still work.
Files changed¶
backend/internal/caddycfg/caddycfg.go-- transport struct gainsVersions []string.reverseProxyFromTGalways emits the transport block.backend/internal/caddycfg/transport_test.go(new).docs/operations/troubleshooting.md-- new "WebSocket backend shows blank UI" entry with symptom catalog, verify command, and three post-fix escalation paths.
Upgrade¶
Caddy reload happens automatically on panel restart (the panel re-pushes the full config via the admin API). HTTPS-upstream WebSocket backends should be reachable on the next browser refresh.
Not changed¶
argos user reset-passwordCLI from v1.3.11 unchanged.- AppSec block-mode CRS coverage from v1.3.12 unchanged.
- v1.3.13 expect_status validation messaging unchanged.
- No DB migrations, no new env vars, no new compose surface.