Skip to content

fix: propagate pre-endpoint errors in sse_client instead of deadlocking#2340

Draft
maxisbey wants to merge 1 commit intomainfrom
fix/sse-client-deadlock-447
Draft

fix: propagate pre-endpoint errors in sse_client instead of deadlocking#2340
maxisbey wants to merge 1 commit intomainfrom
fix/sse-client-deadlock-447

Conversation

@maxisbey
Copy link
Contributor

Fixes #447

Problem

sse_client launches sse_reader via tg.start(), which blocks until task_status.started() is called. When an error occurs before the endpoint event is received, the except Exception handler tries to send the exception to read_stream_writer. The stream has buffer size 0 and nobody is reading (the caller is still blocked in tg.start()), so send() blocks forever.

This deadlocks in three scenarios:

  • Server sends an endpoint event with a mismatched origin (the original report)
  • A network error occurs while waiting for the endpoint event
  • Server sends a message event before the endpoint event (protocol violation)

Fix

Track whether task_status.started() has fired with a started flag. Before it fires, re-raise exceptions so they propagate through tg.start() to the caller. After it fires, send them to the stream as before — the caller is reading by then.

This generalizes the approach from #975, which fixed the same deadlock but only for SSEError. The dedicated SSEError handler is removed since the flag now covers all pre-endpoint exceptions uniformly.

Behavior change

Previously, SSEError raised mid-stream (after the endpoint was received) would crash the task group. Now it is delivered on the read stream like other post-endpoint errors, letting the session layer handle it. This aligns with how stdio and websocket transports surface mid-stream errors.

Testing

Four new regression tests in tests/shared/test_sse.py cover:

  • Endpoint origin mismatch raises ValueError promptly
  • Connection error before endpoint raises promptly
  • Message-before-endpoint raises RuntimeError promptly
  • Post-endpoint errors are delivered via the read stream

All tests use anyio.fail_after(5) guards — if the deadlock regresses, tests fail with timeout rather than hanging CI.

AI Disclaimer

@maxisbey maxisbey force-pushed the fix/sse-client-deadlock-447 branch 4 times, most recently from 9d53554 to d0c7a71 Compare March 24, 2026 22:35
When sse_reader encounters an error before receiving the endpoint event,
the except handler tried to send the exception to read_stream_writer.
With a zero-buffer stream and no reader (the caller is still blocked in
tg.start() waiting for task_status.started()), send() blocks forever.

Track whether started() has fired. Before it, re-raise so the exception
propagates through tg.start(). After it, send to the stream as before.

This also adds a guard for the case where a server sends a message event
before the endpoint event, which would deadlock on the same send() call.

The dedicated SSEError handler from #975 is removed since the started
flag now handles all pre-endpoint exceptions uniformly.

Github-Issue: #447
@maxisbey maxisbey force-pushed the fix/sse-client-deadlock-447 branch from d0c7a71 to 54f02ed Compare March 24, 2026 22:39
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.

sse_client blocks indefinitely when server has incorrect base URL

1 participant