Skip to content

Handle WebSocket disconnects cleanly in streamer _reader#327

Closed
ccjoeg wants to merge 1 commit intotastyware:masterfrom
ccjoeg:handle-websocket-disconnect
Closed

Handle WebSocket disconnects cleanly in streamer _reader#327
ccjoeg wants to merge 1 commit intotastyware:masterfrom
ccjoeg:handle-websocket-disconnect

Conversation

@ccjoeg
Copy link
Copy Markdown

@ccjoeg ccjoeg commented Mar 27, 2026

Summary

  • Adds StreamerDisconnect exception (with code and reason attributes) to tastytrade.utils
  • Catches WebSocketDisconnect and WebSocketNetworkError in both DXLinkStreamer._reader and AlertStreamer._reader, re-raising as StreamerDisconnect
  • Unwraps StreamerDisconnect from the ExceptionGroup in DXLinkStreamer.__asynccontextmanager__ so callers receive a plain exception
  • Exports StreamerDisconnect from the package __init__.py

Motivation

When the DXFeed WebSocket drops — whether from a network error or a server-requested reconnect (close code 1012, SERVICE_RESTART) — the raw httpx_ws exceptions (WebSocketNetworkError, WebSocketDisconnect) propagate uncaught from _reader. This crashes the anyio TaskGroup and surfaces as an ExceptionGroup with a full traceback:

ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | httpx_ws._exceptions.WebSocketDisconnect: (<CloseReason.SERVICE_RESTART: 1012>, 'Reconnection is required')
    +------------------------------------

This makes it difficult for callers to:

  1. Distinguish routine disconnects from actual errors
  2. Implement appropriate retry strategies (e.g. immediate reconnect for code 1012 vs longer backoff for network failures)
  3. Log cleanly without noisy tracebacks for expected events

Usage

After this change, callers can catch StreamerDisconnect and inspect e.code:

from tastytrade import DXLinkStreamer, StreamerDisconnect

while True:
    try:
        async with DXLinkStreamer(session) as streamer:
            # ... subscribe and listen ...
    except StreamerDisconnect as e:
        if e.code == 1012:
            await asyncio.sleep(5)   # server-requested, reconnect quickly
        else:
            await asyncio.sleep(30)  # network error, back off

Test plan

  • Verify existing streamer tests still pass
  • Verify StreamerDisconnect is raised (not ExceptionGroup) on WebSocket close
  • Verify e.code is populated correctly for WebSocketDisconnect (e.g. 1012)
  • Verify e.code is None and e.reason is "network error" for WebSocketNetworkError

🤖 Generated with Claude Code

When the DXFeed WebSocket drops (network error or server-requested
reconnect with code 1012), the raw httpx_ws exceptions
(WebSocketNetworkError, WebSocketDisconnect) propagate uncaught from
_reader, crash the anyio TaskGroup, and surface as an ExceptionGroup
with a full traceback. This makes it difficult for callers to
distinguish routine disconnects from real errors and to implement
appropriate retry strategies (e.g. fast reconnect for code 1012
SERVICE_RESTART vs longer backoff for network failures).

This change:
- Adds StreamerDisconnect exception with code/reason attributes
- Catches WebSocketDisconnect and WebSocketNetworkError in both
  DXLinkStreamer._reader and AlertStreamer._reader, re-raising as
  StreamerDisconnect
- Unwraps StreamerDisconnect from ExceptionGroup in
  DXLinkStreamer.__asynccontextmanager__ so callers receive a plain
  exception they can match on
- Exports StreamerDisconnect from the package for caller use

Callers can now catch StreamerDisconnect and inspect e.code to choose
retry behavior, e.g. immediate reconnect for code 1012 vs 30s backoff
for network errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@ccjoeg ccjoeg closed this Mar 27, 2026
@Graeme22
Copy link
Copy Markdown
Member

Please use the pattern from the docs: https://tastyworks-api.readthedocs.io/en/latest/data-streamer.html#auto-retries

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants