Recover from a btcdata restart without reloading the page
pull-request / validate (pull_request) Successful in 24s

htmx's WebSocket extension reconnects only after close codes 1006/1011/1012/1013.
_ws_proxy closed the browser socket with a normal 1000 on every upstream exit, so
htmx read a btcdata restart as a deliberate shutdown and left both panels frozen
until the visitor reloaded. New browser connections always worked, which hid the
defect from non-browser checks.

The proxy now reconnects upstream behind the still-open browser socket on a bounded
schedule, resets that schedule after a connection that lasted long enough to count
as healthy, and closes with 1012 only once the schedule runs out, so recovery falls
back to htmx's own backoff. A concurrent reader detects a departed browser, which a
dead upstream would otherwise be indistinguishable from, and an unrenderable payload
is logged and skipped rather than ending the stream.

tests/test_ws_proxy.py covers this against a real loopback WebSocket server: same
socket recovery across an upstream restart for both streams, the retryable close
code when the upstream stays down, and skip-on-unrenderable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-11 09:21:19 +02:00
co-authored by Claude Opus 5
parent dd7058b6bd
commit 4a2d8fabad
4 changed files with 280 additions and 18 deletions
+21
View File
@@ -72,6 +72,27 @@ and `/api/strike/ws`, converts each JSON message to an HTML fragment (`price_jso
`strike_json_to_html`) and forwards it. `BTCDATA_URL` is the single point of contact;
`_btcdata_ws_url` rewrites its scheme to `ws`/`wss`.
### Recovering from a btcdata restart
htmx's WebSocket extension reconnects only after an abnormal close
(1006/1011/1012/1013); a normal 1000 close means "the server is done" and the panel stays
frozen until the visitor reloads. So a btcdata restart must never reach the browser as a
normal close:
- `_ws_proxy` reconnects upstream behind the still-open browser socket, on the bounded
`UPSTREAM_RETRY_DELAYS_SECONDS` schedule. An ordinary restart is invisible to the page —
the last values stay on screen and updates resume.
- A connection that lasted `UPSTREAM_STABLE_SECONDS` resets that schedule, so a long-lived
page treats a later restart as a fresh outage rather than a continuing one.
- When the schedule runs out, the proxy closes with `BROWSER_RETRY_CLOSE_CODE` (1012) and
htmx's own backoff takes over. Every server-side exit uses that code; only a browser that
has already left can see a plain close.
- A payload that cannot be rendered is logged and skipped, not treated as a dead stream.
`tests/test_ws_proxy.py` covers this against a real loopback WebSocket server, including
same-socket recovery across an upstream restart and the retryable close code. The regression
to watch for is a 1000 close after upstream loss.
## Source Layout
btcprice acts as a WebSocket proxy between the browser and btcdata: