Hacktoberfest 2026: die Issues, die Maintainer für den Oktober markiert haben – offen und einsteigerfreundlich. Hacktoberfest-Issues durchsuchen

Bug: AssertionError: Request already responded to — cancellation race in v1.27.0

Offen
#2,416 14 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Anfängerfreundlichkeit
68/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
python
Bereich
api, backend

Rechercherichtung

Beginne in mcp/shared/session.py bei RequestResponder.respond() und in mcp/server/lowlevel/server.py bei _handle_request sowie beim Cancellation-Handler um die Zeilen 403-406. Reproduziere die Race Condition, indem du einen lang laufenden tools/call-Handler abschließt, während du notifications/cancelled sendest, und untersuche anschließend, wie der gleichzeitige Abschluss behandelt wird. Erledigt ist die Aufgabe, wenn der Server keine AssertionError mehr auslöst und die Cancellation höchstens eine Antwort erzeugt, ohne laufende Arbeit zum Absturz zu bringen.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

bug fix proposed P2 ready for work
Initial Checks
Description

Bug: AssertionError: Request already responded to — cancellation race in v1.27.0

AssertionError: Request already responded to when CancelledNotification arrives after handler completes but before respond()

Description

When a client sends a notifications/cancelled for a request whose handler has already finished executing but hasn't yet called message.respond(), the server crashes with AssertionError: Request already responded to.

PR #2334 (v1.27.0) fixed the ClosedResourceError crash path by catching CancelledError in _handle_request and guarding respond() against BrokenResourceError/ClosedResourceError. However, it left a race window between handler completion and respond() where a cancellation notification can set _completed = True first, causing the assert on line 129 of session.py to fire.

Reproduction scenario

  1. Client sends a tools/call request with a long-running handler (e.g. polling with 600s timeout)
  2. Handler completes and returns a result
  3. Between the handler's return and the await message.respond(response) call in _handle_request, the client sends notifications/cancelled for that same request ID
  4. The cancellation notification handler (session.py:403-406) calls responder.cancel(), which:
    • Calls _cancel_scope.cancel()
    • Sets _completed = True
    • Sends an error response "Request cancelled"
  5. Back in _handle_request, execution reaches await message.respond(response) at server.py:800
  6. respond() hits assert not self._completed at session.py:129crash

The cancel_scope.cancel() only raises CancelledError if the task is currently in an await. Since the handler already returned, the code path between the handler return and respond() is synchronous — no checkpoint where CancelledError can be delivered. The except anyio.get_cancelled_exc_class() guard at server.py:773 never fires.

Stack trace

ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
    +-+---------------- 1 ----------------
      | ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File "mcp/server/lowlevel/server.py", line 703, in _handle_message
        |     await self._handle_request(message, req, session, lifespan_context, raise_exceptions)
        |   File "mcp/server/lowlevel/server.py", line 800, in _handle_request
        |     await message.respond(response)
        |   File "mcp/shared/session.py", line 129, in respond
        |     assert not self._completed, "Request already responded to"
        | AssertionError: Request already responded to
        +------------------------------------

Impact

This crashes the entire MCP server process, killing all in-flight requests. In our case, the server manages multiple long-running background agents, so a crash loses all active work. The crash is triggered by normal client behavior (user cancels an operation), making it a reliability issue rather than an edge case.

Related

  • #2328 — original ClosedResourceError report
  • #2334 — v1.x fix (covers ClosedResourceError but not this assert race)
  • #2306 — main branch transport-close cancellation
Example Code
The race window in `_handle_request` (`server.py:719`):


# Line 770: handler completes, returns response
response = await handler(req)

# ... exception handling ...

# Line 799-800: GAP — between handler return and respond(),
# a CancelledNotification can arrive on another task and call
# responder.cancel(), setting _completed = True and sending
# an error response. No await in this gap means no CancelledError
# can be delivered.
try:
    await message.respond(response)  # <-- assert fires here
except (anyio.BrokenResourceError, anyio.ClosedResourceError):
    ...


The `except anyio.get_cancelled_exc_class()` at line 773 correctly handles the case where the cancellation arrives *during* handler execution. But it cannot handle cancellation that arrives *after* the handler returns, because there's no async checkpoint between the handler return and `respond()`.

## Suggested fix

In `session.py`, change `respond()` to handle the already-completed case gracefully instead of asserting:


async def respond(self, response: SendResultT | ErrorData) -> None:
    if not self._entered:
        raise RuntimeError("RequestResponder must be used as a context manager")
    
    # If already completed (e.g. by a concurrent cancellation), skip silently.
    if self._completed:
        return

    if not self.cancelled:
        self._completed = True
        await self._session._send_response(
            request_id=self.request_id, response=response
        )


Alternatively, the guard could be added in `_handle_request` before calling `respond()`:


if not message._completed:
    try:
        await message.respond(response)
    except (anyio.BrokenResourceError, anyio.ClosedResourceError):
        logger.debug("Response for %s dropped - transport closed", message.request_id)


The first approach (in `respond()` itself) is more robust since it closes the race for all callers.
Python & MCP Python SDK
- `mcp` 1.27.0
- Python 3.14
- anyio (asyncio backend)
- FastMCP stdio transport
- Client: Claude Code 2.1.92
Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 11 Std.
Gemergte PRs (30 T.)
30

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus modelcontextprotocol/python-sdk

Alle Issues in modelcontextprotocol/python-sdk

Ähnliche Issues

Weitere Issues zu Python

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.