Skip to content

fix: distinguish AbortSignal cancellation from a real timeout - #3046

Open
vignes-vm wants to merge 6 commits into
modelcontextprotocol:mainfrom
vignes-vm:fix/2165-distinct-abort-error-code
Open

vignes-vm wants to merge 6 commits into
modelcontextprotocol:mainfrom
vignes-vm:fix/2165-distinct-abort-error-code

Conversation

@vignes-vm

Copy link
Copy Markdown

Fixes #2165.

Cancelling a request via AbortSignal (controller.abort()) currently rejects with SdkErrorCode.RequestTimeout — the same code a real timeout uses — so callers can't tell a deliberate abort apart from the request's own timeout elapsing. I ran into exactly this while building a project on top of the SDK: I wanted "retry on timeout, don't retry on a user-cancelled abort" and both looked identical.

What changed

  • Added SdkErrorCode.RequestAborted, distinct from RequestTimeout.
  • Updated every place that wraps a non-SdkError abort reason in a fallback SdkError to use the new code instead of RequestTimeout:
    • Protocol.request()'s already-aborted pre-check and its in-flight cancel() path (packages/core-internal/src/shared/protocol.ts)
    • Client's response-cache warm-hit short-circuit (_serveFromCache) and listen()'s already-aborted pre-check (packages/client/src/client/client.ts)
  • The actual timeout path is untouched: timeoutHandler already passes a pre-wrapped SdkError(RequestTimeout, ...) before it ever reaches the fallback wrap, so it's unaffected — verified with a new test that fires a real timeout and asserts it's still RequestTimeout.
  • Updated the tests that were pinning the old (conflated) behavior, and added a new one that aborts well before a 60s timeout and asserts the rejection is RequestAborted, not RequestTimeout.
  • Updated the two docs spots that described this behavior (skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.md and the SdkErrorCode enum table in references/errors.md), and added a changeset.

Verification

  • pnpm --filter @modelcontextprotocol/core-internal run typecheck / lint / test — 1527 tests pass
  • pnpm --filter @modelcontextprotocol/client run typecheck / lint / test — 998 tests pass
  • pnpm --filter @modelcontextprotocol/test-integration run test — 381 tests pass
  • pnpm --filter @modelcontextprotocol/test-e2e run test had 8 pre-existing failures (fake-timer related, in protocol:timeout:max-total / protocol:progress:callback / tools:call:progress scenarios) that I confirmed also fail identically on a clean main checkout before my change — unrelated to this fix.
  • Full check:skills / sync:snippets --check pass; the repo's own pre-push hook (build + typecheck + lint across all 52 workspace packages) passed on push.

I looked for other RequestTimeout-on-abort sites beyond what the issue pointed at (cancel() in protocol.ts) and found two more with the identical pattern (_serveFromCache, listen()'s pre-check) — fixed those too for consistency, since leaving them would mean only some abort paths report the new code.

Aborting a request via AbortSignal rejected with SdkErrorCode.RequestTimeout,
the same code a real timeout uses, so callers had no way to tell a deliberate
controller.abort() apart from the request actually timing out. Add a
dedicated SdkErrorCode.RequestAborted and use it everywhere a non-SdkError
abort reason gets wrapped: Protocol.request()'s pre-check and in-flight
cancel() path, the response-cache warm-hit short-circuit, and
Client.listen()'s pre-check. The request's own timeout path already passes a
pre-wrapped SdkError and is unaffected.

Fixes modelcontextprotocol#2165.
@vignes-vm
vignes-vm requested a review from a team as a code owner October 18, 2026 08:07
@changeset-bot

changeset-bot Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a6c4636

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@3046

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@3046

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@3046

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@3046

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@3046

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@3046

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@3046

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@3046

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@3046

commit: a6c4636

@claude claude Bot added the v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes label Oct 18, 2026
…ancellation

Update sleep helper in inputRequiredDriver to wrap abort signals into SdkError with RequestAborted rather than RequestTimeout, ensuring multi round trip pacing cancellations match protocol level abort behavior. Add test coverage for pacing abort rejections.

Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
Pass the original abort reason to SdkError cause across Protocol and Client fallback wrapping points so custom cancellation payloads and DOMExceptions are not swallowed. Add unit tests asserting cause preservation and standard DOMException handling.

Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
…cellation

Verify that calling Client request methods with an aborted signal rejects with RequestAborted alongside the existing timeout assertions.

Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
…able

Add Request cancelled via AbortSignal to the migration error mapping table indicating the transition from RequestTimeout to RequestAborted in v2.

Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
@ManoharPaturi

Copy link
Copy Markdown

Hey @vignes-vm, awesome work putting this PR together for issue 2165.

I took a deep dive into the rest of the codebase to see if any other abort paths were still conflating cancellations with timeouts. I noticed that inputRequiredDriver pacing sleep during multi round trip requests was still rejecting with RequestTimeout on abort. In addition, wrapping non SdkError abort reasons was dropping the underlying cause, which loses context when someone cancels with a custom error or standard DOMException.

I went ahead and put together four focused improvements that build right on top of your work:

  1. Fixed the pacing sleep abort fallback in inputRequiredDriver so it rejects with RequestAborted instead of RequestTimeout.

  2. Preserved the original abort reason on the cause property across protocol, client, and driver abort handlers so errors retain full context.

  3. Added integration test coverage verifying that standard client calls like listResources reject with RequestAborted when cancelled.

  4. Updated the v1 to v2 migration error reference table in the documentation to document RequestAborted.

All tests pass cleanly across core internal, client, and integration packages, alongside full lint and typecheck verification.

I pushed the four commits to my fork here if you would like to inspect or pull them directly:
https://github.com/ManoharPaturi/typescript-sdk/tree/fix/2165-distinct-abort-error-code

If you are open to teaming up on this, I would love to contribute these to the PR and join you as a coauthor. Let me know what you think and how you would prefer to pull the commits in!

@vignes-vm

Copy link
Copy Markdown
Author

@ManoharPaturi thanks — pulled all 4 in and pushed, they're on the PR now. The inputRequiredDriver pacing-sleep catch is a real gap I'd missed (I only checked that file's test setup, not its own internal abort-wrap), and the cause-preservation is a solid improvement — String(reason) was dropping the original DOMException/Error entirely.

Re-verified the full combined branch before pushing: core-internal (1529 tests), client (998 tests), test-integration (381 tests), typecheck/lint on both packages, sync:snippets, check:skills — all green, plus the repo's own pre-push hook (build + typecheck + lint across all 52 workspace packages). Fast-forwarded cleanly, no conflicts.

Appreciate you digging into the rest of the abort paths — good catch.

@0xamlab

0xamlab commented Oct 18, 2026

Copy link
Copy Markdown

Verified the fix locally. On main, aborting a request still rejects with REQUEST_TIMEOUT; on this branch it rejects with REQUEST_ABORTED, and the new test at packages/core-internal/test/shared/abortReasonPassthrough.test.ts:87 passes. Ran the full package suites: @modelcontextprotocol/core-internal 1529 passed, @modelcontextprotocol/client 998 passed. The mechanism is the new SdkErrorCode.RequestAborted at packages/core-internal/src/errors/sdkErrors.ts:33 plus the wrap sites in packages/core-internal/src/shared/protocol.ts:1425 and packages/core-internal/src/shared/protocol.ts:1521. Looks good.

@ManoharPaturi

Copy link
Copy Markdown

Thanks for taking the time to review and verify locally, really appreciate it!

This branch has not been deployed

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

Labels

v2 Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AbortSignal cancellation throws SdkErrorCode.RequestTimeout instead of a distinct abort error

3 participants