fix(api): log socket disconnects at the same level as connects - #9438
Open
lstein wants to merge 1 commit into
Open
fix(api): log socket disconnects at the same level as connects#9438lstein wants to merge 1 commit into
lstein wants to merge 1 commit into
Conversation
Socket connects for authenticated clients were logged at INFO while disconnects were logged at DEBUG. With only half the lifecycle visible, a client that reconnects in a loop — a backgrounded browser tab whose timers have been throttled, a flaky network — is indistinguishable in the log from sockets accumulating without bound. Log the disconnect at the same level as the matching connect, and include python-socketio's disconnect reason (`ping timeout`, `transport close`, ...), which is the first thing worth knowing when sockets are churning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lstein
requested review from
JPPhoto,
Pfannkuchensack,
blessedcoolant and
dunkeroni
as code owners
August 2, 2026 01:46
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Fix (logging/observability). Socket connects for authenticated clients are logged at INFO, while disconnects are logged at DEBUG:
With only half the lifecycle visible at the default log level, a client that reconnects in a loop is indistinguishable from sockets accumulating without bound. What it looks like in practice:
That reads like a leak. It was actually a forgotten browser tab on an idle machine: the browser had throttled its timers to roughly one wake per minute, so the socket died and was re-established on that cadence. Diagnosing it took a TCP-level packet-state capture on the server, which is a silly amount of work for a question the log should have answered directly.
The change: log the disconnect at the same level as the matching connect (INFO for token-authenticated sockets, DEBUG for the single-user path, mirroring the two halves of
_handle_connect), and include python-socketio's disconnect reason. With this, the same incident reads:reasonis one ofping timeout,transport close,transport error,client disconnect,server disconnect— the first thing worth knowing when sockets churn, and it distinguishes a throttled/asleep client from a network problem from a deliberate client teardown.Note the default log level is unchanged for a default (single-user) install:
useSocketIO.tssends no token in single-user mode, so those sockets take theauthenticated: Falsebranch and both halves stay at DEBUG. Only multiuser deployments — which already log every connect at INFO — gain a line.Related Issues / Discussions
None.
QA Instructions
Multiuser mode (
multiuser: true):Socket ... connected with user_id: ...at INFO as before.Socket ... disconnected (user_id: ..., reason: client disconnect)should appear at INFO.ping timeoutortransport close.Single-user mode: both connect and disconnect stay at DEBUG. Run with
log_level: debugto confirm the pair appears, and at the default level to confirm neither does.Automated:
pytest tests/app/test_workflow_socketio.py tests/app/routers/test_multiuser_authorization.py— 4 new tests cover the INFO pairing, the DEBUG pairing, an unknown sid, and a call without areasonargument.Merge Plan
Ordinary merge, no migrations, no schema changes.
Notes for reviewers
Two constraints on
_handle_disconnect's body are documented in its docstring because they are non-obvious and easy to break in a later edit — both were found by adversarially reviewing this diff against the installed python-socketio:AsyncServer._handle_disconnectdoes not guard its_trigger_eventcall, so an exception in this handler skipsmanager.disconnect()and leaks the sid's room membership andserver.environentry for the life of the process. Hence.get()rather than subscripting the per-socket dict, whose shape is enforced only by convention.popmust stay first._trigger_eventretriesdisconnecthandlers onTypeErrorwith one fewer argument, and that retry wraps the await of the handler, not just the argument binding — so aTypeErrorraised anywhere in the body would silently re-enter the method. Popping first makes the re-entry a no-op.reasonkeeps aNonedefault becausepython-socketiois unpinned inpyproject.toml; versions before 5.12 call the handler withsidalone.Out of scope, but found while reviewing this:
_handle_sub_queuecan resurrect a popped_socket_usersentry.AsyncServer.async_handlersdefaults toTrue, sosubscribe_queueruns as a background task whoseis_connectedcheck happened before the task was scheduled; if the transport dies in between, the disconnect pops the entry and the queued task then recreates it at the single-user fallback, where nothing will ever pop it again. The stale sid then pollutes every subsequentskip_sidlist. It predates this PR and is untouched here — happy to file it separately.Checklist
What's Newcopy (if doing a release after this PR) — n/a