Repository navigation
feat(fastapi): CancelOnDisconnectRoute - cancel the handler (and its query) when the client disconnects - #58
Merged
Merged
Conversation
…lient leaves (0.16.0) ASGI servers such as Granian don't cancel a request handler on disconnect, so fetch_all-based endpoints ran to completion after a frontend abort. The opt-in route class runs the handler as a task and cancels it on http.disconnect; psycopg/pgdevkit then cancel the statement on the server. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…gone before the handler starts request.body() raised ClientDisconnect there, which surfaced as a 500 with a traceback. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ield dependencies roll back, keep outer cancellation Review findings on CancelOnDisconnectRoute: - request.body() buffered the whole body before dependencies ran (memory pinning by unauthenticated large uploads). The handler now gets a Request fed by a bounded reader queue that also spots the disconnect. - A yield dependency saw a clean exit after a cancelled handler; the route now raises HTTPException(499), so transactions roll back. - _cancel swallowed an outer cancellation arriving during the handler's cleanup. - A failing receive() was reported as a disconnect (499). - Docs: dependencies/BackgroundTasks are cancelled too, SKILL description and checklist, cross references from the fetch_all/cancel docs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…an asyncio timeout scope The separate handler task broke yield dependencies that set/reset context variables (ValueError: Token created in a different Context, a 500 on every request) and gave exception handlers a Request whose body the reader had already taken (they hung on request.body()). Cancelling via asyncio.timeout(None) + reschedule, as PostgresJsonResponse does, keeps one task and one Request object and removes the cleanup-wait helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… front with an empty 499 BaseHTTPMiddleware fails with 'RuntimeError: No response returned.' (traceback in the log) when the client disconnects before the first byte and nothing was sent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
aersam
marked this pull request as ready for review
October 10, 2026 05:18
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.
Problem
Granian (our server) does not cancel a request handler when the client disconnects (verified over a real socket). Only
PostgresJsonResponsewatches for the disconnect;fetch_all/raw-cursor/Meili based endpoints run to completion, so frontendAbortControlleraborts (debounced search, grid filter changes) don't free the database.Change
pgdevkit.fastapi.CancelOnDisconnectRoute(APIRouter(route_class=...)): cancels the handler when the client disconnects (in the request's own task, via anasyncio.timeoutcancel scope as inPostgresJsonResponse); psycopg/pgdevkit then cancel the running statement. The handler'sRequestgets areceive()fed by a small bounded reader queue, so the body is not buffered. The gone client gets a 499 (anHTTPException, soyielddependencies roll back, andBaseHTTPMiddlewarestays happy).BaseHTTPMiddlewarein the stack): disconnect cancels query, plain routes unaffected, big/unread bodies, yield dependencies see a failure, context-var dependencies work, exception handlers can read the body, outer cancellation during cleanup is not swallowed, finished-handler-wins race. Mutation-checked.asyncio.shield.Reviewed with
/bms-code-review --full(correctness, security, backend, docs); findings addressed. Known, not fixed here (separate issue, pre-existing injson_response.py): withBaseHTTPMiddlewarein front, an abortedPostgresJsonResponsebefore its first byte logsRuntimeError: No response returned; the disconnect watcher there can callrescheduleon a finished timeout.🤖 Generated with Claude Code