[v1.x] fix: guard respond() and cancel() against concurrent cancellation race (#2416)
Closed
SH
3mo ago+32+32+01234567890123456789-2-2-0123456789
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
- Tests added/updated
- Docs updated (not user-facing — internal race fix)
- Lint/format passes
- Linked issue referenced (Fixes #2416)
- Commits signed off (DCO)
- No unrelated changes bundled in