_ws_proxy closed the browser socket with a plain 1000 in its finally block on every
upstream exit, so htmx read the restart as a deliberate server shutdown and never reconnected.
A new browser connection always worked, which is why non-browser checks looked healthy and
hid the defect.
Change
_ws_proxy now reconnects upstream behind the still-open browser socket instead of tearing
the page's socket down:
bounded retry schedule (UPSTREAM_RETRY_DELAYS_SECONDS), reset after a connection that
survived UPSTREAM_STABLE_SECONDS, so a long-lived page treats a later restart as a fresh
outage rather than a continuing one
a concurrent reader task detects a departed browser — otherwise a dead upstream and a
departed client are indistinguishable while the proxy is not sending
only once the schedule runs out does it close with 1012, handing recovery to htmx's own
backoff; every server-side exit uses that code, so 1000 can no longer reach a live page
a payload that cannot be rendered is logged once per connection and skipped, rather than
ending the stream
The transforms are untouched: price_json_to_html and strike_json_to_html are unchanged,
so the rendering contract consumed by rustbtcdata's frozen fixtures still holds.
Tests
tests/test_ws_proxy.py runs a real loopback WebSocket server as the stand-in btcdata, since
what is being tested is connection lifecycle rather than rendering:
same-socket recovery across an upstream restart, for /ws/price and /ws/strike
the retryable close code when the upstream never comes back, and when it is lost mid-stream
an unrenderable message does not end the stream
make test: 12 passed. All five new cases fail against the pre-fix proxy.
Verified end to end against the real Rust market-data backend and the real database: a held
proxy socket kept streaming across a genuine backend restart, and a fresh isolated Chrome
profile resumed price updates after the restart with browser_sockets: {"opened": 2, "closed": []} — no browser socket closed at all — and zero console, page or CDN errors.
Note on merging
Merging starts build.yml, the Harbor image publish and the Flux deployment. That is
intended: shipping this image is a recorded prerequisite for pointing production btcprice at
the Rust backend, because without it every backend restart freezes production panels.
## Problem
A btcdata restart left both panels frozen until the visitor reloaded the page.
htmx's WebSocket extension reconnects a browser socket only after close codes
1006/1011/1012/1013:
```js
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1011, 1012, 1013].indexOf(e.code) >= 0) { ... }
```
`_ws_proxy` closed the browser socket with a plain `1000` in its `finally` block on every
upstream exit, so htmx read the restart as a deliberate server shutdown and never reconnected.
A *new* browser connection always worked, which is why non-browser checks looked healthy and
hid the defect.
## Change
`_ws_proxy` now reconnects upstream behind the still-open browser socket instead of tearing
the page's socket down:
- bounded retry schedule (`UPSTREAM_RETRY_DELAYS_SECONDS`), reset after a connection that
survived `UPSTREAM_STABLE_SECONDS`, so a long-lived page treats a later restart as a fresh
outage rather than a continuing one
- a concurrent reader task detects a departed browser — otherwise a dead upstream and a
departed client are indistinguishable while the proxy is not sending
- only once the schedule runs out does it close with `1012`, handing recovery to htmx's own
backoff; every server-side exit uses that code, so `1000` can no longer reach a live page
- a payload that cannot be rendered is logged once per connection and skipped, rather than
ending the stream
The transforms are untouched: `price_json_to_html` and `strike_json_to_html` are unchanged,
so the rendering contract consumed by rustbtcdata's frozen fixtures still holds.
## Tests
`tests/test_ws_proxy.py` runs a real loopback WebSocket server as the stand-in btcdata, since
what is being tested is connection lifecycle rather than rendering:
- same-socket recovery across an upstream restart, for `/ws/price` and `/ws/strike`
- the retryable close code when the upstream never comes back, and when it is lost mid-stream
- an unrenderable message does not end the stream
`make test`: 12 passed. All five new cases fail against the pre-fix proxy.
Verified end to end against the real Rust market-data backend and the real database: a held
proxy socket kept streaming across a genuine backend restart, and a fresh isolated Chrome
profile resumed price updates after the restart with `browser_sockets: {"opened": 2,
"closed": []}` — no browser socket closed at all — and zero console, page or CDN errors.
## Note on merging
Merging starts `build.yml`, the Harbor image publish and the Flux deployment. That is
intended: shipping this image is a recorded prerequisite for pointing production btcprice at
the Rust backend, because without it every backend restart freezes production panels.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
magnus
merged commit ac01388117 into main2026-10-11 07:26:42 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem
A btcdata restart left both panels frozen until the visitor reloaded the page.
htmx's WebSocket extension reconnects a browser socket only after close codes
1006/1011/1012/1013:
_ws_proxyclosed the browser socket with a plain1000in itsfinallyblock on everyupstream exit, so htmx read the restart as a deliberate server shutdown and never reconnected.
A new browser connection always worked, which is why non-browser checks looked healthy and
hid the defect.
Change
_ws_proxynow reconnects upstream behind the still-open browser socket instead of tearingthe page's socket down:
UPSTREAM_RETRY_DELAYS_SECONDS), reset after a connection thatsurvived
UPSTREAM_STABLE_SECONDS, so a long-lived page treats a later restart as a freshoutage rather than a continuing one
departed client are indistinguishable while the proxy is not sending
1012, handing recovery to htmx's ownbackoff; every server-side exit uses that code, so
1000can no longer reach a live pageending the stream
The transforms are untouched:
price_json_to_htmlandstrike_json_to_htmlare unchanged,so the rendering contract consumed by rustbtcdata's frozen fixtures still holds.
Tests
tests/test_ws_proxy.pyruns a real loopback WebSocket server as the stand-in btcdata, sincewhat is being tested is connection lifecycle rather than rendering:
/ws/priceand/ws/strikemake test: 12 passed. All five new cases fail against the pre-fix proxy.Verified end to end against the real Rust market-data backend and the real database: a held
proxy socket kept streaming across a genuine backend restart, and a fresh isolated Chrome
profile resumed price updates after the restart with
browser_sockets: {"opened": 2, "closed": []}— no browser socket closed at all — and zero console, page or CDN errors.Note on merging
Merging starts
build.yml, the Harbor image publish and the Flux deployment. That isintended: shipping this image is a recorded prerequisite for pointing production btcprice at
the Rust backend, because without it every backend restart freezes production panels.
🤖 Generated with Claude Code