Skip to content

[v1.x] fix: guard respond() and cancel() against concurrent cancellation race (#2416)#3088

Open
shivamsingh-007 wants to merge 1 commit into
modelcontextprotocol:v1.xfrom
shivamsingh-007:fix/respond-cancel-race
Open

[v1.x] fix: guard respond() and cancel() against concurrent cancellation race (#2416)#3088
shivamsingh-007 wants to merge 1 commit into
modelcontextprotocol:v1.xfrom
shivamsingh-007:fix/respond-cancel-race

Conversation

@shivamsingh-007

Copy link
Copy Markdown

Summary

When a client sends
otifications/cancelled\ after the handler has returned
but before
espond()\ is called, the server crashes with
\AssertionError: Request already responded to.

Related Issue

Fixes #2416

What changed

  • \RequestResponder.respond()\ — replace \�ssert not self._completed\ with
    a silent early return, so a concurrent cancellation that already errored
    is not double-replied to.
  • \RequestResponder.cancel()\ — early-return if _completed\ is already set,
    so the reverse ordering (respond then cancel) is also safe.
  • New regression test \ est_respond_after_cancel_does_not_raise.

Why this approach

The root cause is a race window between handler return and
espond().
There is no \�wait\ checkpoint in that gap, so \CancelledError\ cannot be
delivered to the handler task. The fix makes
espond()\ idempotent:
whichever path (\cancel()\ via the notification handler or
espond()\ via
_handle_request) sets _completed\ first wins; the other path silently
no-ops. This is safe under cooperative multitasking since _completed\ is
set synchronously before the first \�wait\ in each method.

Testing done

  • New regression test drives a \RequestResponder\ through cancel-then-respond
    and asserts no exception is raised
  • Existing \ est_request_cancellation\ and \ est_connection_closed\ still pass
  • Lint (ruff), format, and type check (pyright) all clean

Checklist

When a CancelledNotification arrives after the handler has returned
but before respond() is called, cancel() sets _completed=True and
sends an error response. respond() then hits assert not self._completed
and crashes the server.

Replace the assert with a silent early return in respond(), and add
the same guard to cancel() so duplicate completion is harmless either
way. Either path wins exactly once under cooperative multitasking.

Fixes modelcontextprotocol#2416

Signed-off-by: Shivam Singh <shivam.singh.koyad2006@gmail.com>
Github-Issue: modelcontextprotocol#2416
Copilot AI review requested due to automatic review settings July 11, 2026 13:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants