Skip to main content

Cancel FastAPI request handlers when the client disconnects

Project description

fastapi-disconnect

Package version Supported Python versions

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 StreamingResponse body 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 with TypeError.
  • The middleware buffers request bodies eagerly by default — set queue_size to 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.disconnect event may arrive late or never.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_disconnect-0.4.0.tar.gz (39.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_disconnect-0.4.0-py3-none-any.whl (9.2 kB view details)

Uploaded Python 3

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

Hashes for fastapi_disconnect-0.4.0.tar.gz
Algorithm Hash digest
SHA256 d60a559823a37f5dc8395933e0f219509af64f0939a98a59ca2a96d27ea3996b
MD5 977a82daf6fbd9704a579b9908a6161c
BLAKE2b-256 cd2a9a67bf4f6d54b2249cf603a0bc3c40dfc74f9f30ca3641926119cd98b833

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_disconnect-0.4.0.tar.gz:

Publisher: publish.yml on qweeze/fastapi-disconnect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fastapi_disconnect-0.4.0-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_disconnect-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ad5444cfc5c946f9e1238837e156de73fe3104e387a249400e3dd655da1557a4
MD5 8c7b39080ebe53dbbc089348c23c2ce1
BLAKE2b-256 a3a1bdc7bc7109c9d3950415304ca2698ce51911487c2e298ec6a5807a367d24

See more details on using hashes here.

Provenance

The following attestation bundles were made for fastapi_disconnect-0.4.0-py3-none-any.whl:

Publisher: publish.yml on qweeze/fastapi-disconnect

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page