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

Closed
SH
3mo ago
+32-2

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