Fix: Ensure on_client_disconnected event always fires in FastAPIWebsocketTransport #2762
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Problem
This PR fixes an issue where the
on_client_disconnectedcallback would not fire in approximately 5% of calls when usingFastAPIWebsocketTransportwith Telnyx (and potentially other telephony providers).Root Cause
The issue is a race condition with task cancellation that occurs when a remote client disconnects:
_receive_messages()loop exitstrigger_client_disconnected()to fire the eventstop()is called (due to pipeline cleanup)stop()→_stop_tasks()→ cancels the_receive_tasktrigger_client_disconnected()from runningThis race condition happens intermittently (~5% of the time) depending on the timing between the remote disconnect and the pipeline cleanup.
Background: Intentional Design from Commit 8569b61
This fix respects the intentional design from commit 8569b615 (Sept 15, 2025) which established that:
The issue is NOT with this design decision, but with the implementation being vulnerable to task cancellation.
Solution
The fix uses
asyncio.shield()in afinallyblock to protect the disconnect callback from being cancelled:How asyncio.shield() works
asyncio.shield()protects a coroutine from being cancelled. When the outer task is cancelled:CancelledErroris re-raised for proper cleanupThis ensures:
stop()/cancel()→ event does not fire (respectsis_closingflag)Related
origin/fastapi_disconnect_issuebranch which was created but never merged