Document how a handler deals with cancellation

Merged
MA
14d ago
+348-2

Fixes #3389.

This documents how a handler deals with cancellation using public API. It does not add a cancel_requested event to Context. That event is only set when a notifications/cancelled message arrives, and on 2026-07-28 Streamable HTTP cancelling means closing the request's stream, so a property backed by it would never fire there. Cancelling the handler itself already works on both.

What was added

  • docs/handlers/cancellation.md, a short page under "Inside your handler", after Progress
    • When the client gives up on a call (it cancels, or its timeout runs out), the SDK cancels the handler. Tools, prompts and resource functions alike.
    • An async def tool cleans up in a finally, and cleanup that has to await goes inside anyio.move_on_after(seconds, shield=True).
    • A plain def tool runs in a thread that nothing can interrupt, so it calls anyio.from_thread.check_cancelled() between units of work, and cleans up in a plain finally. One that never checks runs to the end and its result is dropped.
    • A warning for the two Streamable HTTP configurations where the handler is not cancelled today: json_response=True on a 2026-07-28 connection, and stateless_http=True on a legacy one.
  • docs_src/cancellation/tutorial001.py and tutorial002.py, the two examples on the page
  • Links to the page from the section index, the end of the Progress page, and the "async def" section of Tools
  • tests/docs_src/test_cancellation.py

Nothing changes under src/, and the translated pages aren't regenerated here.

How it was checked

  • The tests run both examples against a client that abandons the call, in process and over an in-memory JSON-RPC stream. The async one is also run over Streamable HTTP at both protocol eras, and against a client timeout.
    • The async example's cleanup runs to its last line.
    • The def example stops at its next check (three books requested, one indexed) and its finally puts search back online. Left alone, it indexes all three and cleans up the same way.
    • A def tool that never checks finishes anyway.
    • A prompt and a resource function are cancelled too.
    • Under the two configurations in the warning the tool keeps running.
  • Removing shield=True, the check_cancelled() call, or the def example's finally makes the tests fail.
  • Both examples were also run against a real stdio subprocess and a real uvicorn server, at both protocol eras. The hold was released about 100 ms after the client gave up, which is the example's own simulated round trip, and the index rebuild stopped after the book it was on.
  • ./scripts/test passes with 100% coverage. ruff, pyright, markdownlint and the strict docs build are clean.

AI Disclaimer