Cancel FastAPI request handlers when the client disconnects
Project description
fastapi-disconnect
Cancel FastAPI request handlers when the client disconnects.
By default, when a client disconnects (timeout, closed tab, network failure),
FastAPI keeps executing the request handler to completion. For long-running
handlers — an AI flow making paid LLM calls, a heavy database query — that
means paying for work nobody is waiting for. This library cancels the handler
the moment the client goes away, by listening for the ASGI http.disconnect
event (no polling of request.is_disconnected()).
Installation
pip install fastapi-disconnect
Usage
App-wide, via middleware — protects every route, no handler changes:
from fastapi import FastAPI
from fastapi_disconnect import CancelOnDisconnectMiddleware
app = FastAPI()
app.add_middleware(
CancelOnDisconnectMiddleware,
exclude_paths=[r"/uploads/.*"], # optional; re.fullmatch against the path
on_disconnect=log_cancellation, # optional; sync or async, gets the ASGI scope
queue_size=64, # optional; bounds body buffering (default: unbounded)
)
on_disconnect runs only when a premature disconnect cancelled a request
(not on normal connection close) — a natural place for a metric counting the
work cancellation saved you.
Per-endpoint, via decorator — applied below the route decorator:
from fastapi_disconnect import cancel_on_disconnect
@app.get("/generate")
@cancel_on_disconnect
async def generate() -> str:
return await expensive_llm_flow()
Cooperative, via dependency — for handlers that want to decide when to stop instead of being cancelled preemptively (e.g. finish the current step, checkpoint, then exit):
from fastapi_disconnect import Disconnected
@app.get("/agent")
async def agent(disconnected: Disconnected) -> Result:
for step in plan:
if disconnected.is_set(): # free local check, no polling
return partial_result
await run_step(step)
Disconnected is an annotated alias for
Annotated[asyncio.Event, Depends(disconnected_event)]; the event is set the
moment the client disconnects and can also be awaited (disconnected.wait()).
Use it instead of the decorator/middleware on a route — combined with them,
preemptive cancellation fires first (though the event is still set, which is
handy inside shielded cleanup). All three mechanisms share a single
per-request watcher, so any combination is safe.
On disconnect, the handler receives a regular asyncio.CancelledError at its
next await, so finally blocks and async context managers run as usual.
The decorator produces a 499 Client Closed Request response (which goes
nowhere — the client is gone — but keeps logs meaningful); the middleware
simply stops.
Cleanup on cancellation
Catching CancelledError for cleanup is supported, with one rule: the
cancellation is level-triggered (anyio cancel-scope semantics). Once the
client is gone, every subsequent await inside the handler raises
CancelledError again — so asynchronous cleanup must run in a shielded scope:
import anyio
@app.get("/generate")
@cancel_on_disconnect
async def generate() -> str:
try:
return await expensive_llm_flow()
except asyncio.CancelledError:
with anyio.CancelScope(shield=True):
await release_resources() # runs to completion
raise
An unshielded await in an except CancelledError or finally block is
re-cancelled at its first checkpoint. (asyncio.shield() is not a substitute:
it detaches the inner call, but the awaiting handler is still re-cancelled, so
the cleanup continues without you.) Synchronous cleanup needs no shielding.
Re-raise after cleaning up — a handler that swallows the cancellation and
returns a value is treated as having completed normally.
Caveats
- asyncio only
- Don't read the raw body (
request.body()/request.stream()) inside a guarded handler — the disconnect watcher owns the receive channel while the handler runs. FastAPI-parsed body params are fine, and under the middleware raw reads work normally. - The decorator guards only the handler — dependencies and request
parsing have already run when it starts, and a returned
StreamingResponsebody runs after it exits (the decorator warns). Use the middleware to cover the whole request lifecycle. Sync (def) handlers can't be cancelled and are rejected withTypeError. - The middleware buffers request bodies eagerly by default — set
queue_sizeto restore upload backpressure (disconnect detection pauses while the queue is full), exclude upload routes, or limit request size upstream. - Proxies can hide disconnects — behind a buffering proxy (nginx, ALB)
the
http.disconnectevent may arrive late or never.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_disconnect-0.4.0.tar.gz.
File metadata
- Download URL: fastapi_disconnect-0.4.0.tar.gz
- Upload date:
- Size: 39.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d60a559823a37f5dc8395933e0f219509af64f0939a98a59ca2a96d27ea3996b
|
|
| MD5 |
977a82daf6fbd9704a579b9908a6161c
|
|
| BLAKE2b-256 |
cd2a9a67bf4f6d54b2249cf603a0bc3c40dfc74f9f30ca3641926119cd98b833
|
Provenance
The following attestation bundles were made for fastapi_disconnect-0.4.0.tar.gz:
Publisher:
publish.yml on qweeze/fastapi-disconnect
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_disconnect-0.4.0.tar.gz -
Subject digest:
d60a559823a37f5dc8395933e0f219509af64f0939a98a59ca2a96d27ea3996b - Sigstore transparency entry: 2081954059
- Sigstore integration time:
-
Permalink:
qweeze/fastapi-disconnect@99d87584f14293381f4dbaa59f59f941d342f84d -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/qweeze
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99d87584f14293381f4dbaa59f59f941d342f84d -
Trigger Event:
push
-
Statement type:
File details
Details for the file fastapi_disconnect-0.4.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_disconnect-0.4.0-py3-none-any.whl
- Upload date:
- Size: 9.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad5444cfc5c946f9e1238837e156de73fe3104e387a249400e3dd655da1557a4
|
|
| MD5 |
8c7b39080ebe53dbbc089348c23c2ce1
|
|
| BLAKE2b-256 |
a3a1bdc7bc7109c9d3950415304ca2698ce51911487c2e298ec6a5807a367d24
|
Provenance
The following attestation bundles were made for fastapi_disconnect-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on qweeze/fastapi-disconnect
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fastapi_disconnect-0.4.0-py3-none-any.whl -
Subject digest:
ad5444cfc5c946f9e1238837e156de73fe3104e387a249400e3dd655da1557a4 - Sigstore transparency entry: 2081954068
- Sigstore integration time:
-
Permalink:
qweeze/fastapi-disconnect@99d87584f14293381f4dbaa59f59f941d342f84d -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/qweeze
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@99d87584f14293381f4dbaa59f59f941d342f84d -
Trigger Event:
push
-
Statement type: