Repository navigation
Conversation
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.
🦋 Changeset detectedLatest commit: a6c4636 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
…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>
|
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:
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: 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! |
|
@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. |
|
Verified the fix locally. On main, aborting a request still rejects with |
|
Thanks for taking the time to review and verify locally, really appreciate it! |
Fixes #2165.
Cancelling a request via
AbortSignal(controller.abort()) currently rejects withSdkErrorCode.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
SdkErrorCode.RequestAborted, distinct fromRequestTimeout.SdkErrorabort reason in a fallbackSdkErrorto use the new code instead ofRequestTimeout:Protocol.request()'s already-aborted pre-check and its in-flightcancel()path (packages/core-internal/src/shared/protocol.ts)Client's response-cache warm-hit short-circuit (_serveFromCache) andlisten()'s already-aborted pre-check (packages/client/src/client/client.ts)timeoutHandleralready passes a pre-wrappedSdkError(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 stillRequestTimeout.RequestAborted, notRequestTimeout.skills/mcp-typescript-sdk-upgrade-to-v2/references/behavioral-changes.mdand theSdkErrorCodeenum table inreferences/errors.md), and added a changeset.Verification
pnpm --filter @modelcontextprotocol/core-internal run typecheck/lint/test— 1527 tests passpnpm --filter @modelcontextprotocol/client run typecheck/lint/test— 998 tests passpnpm --filter @modelcontextprotocol/test-integration run test— 381 tests passpnpm --filter @modelcontextprotocol/test-e2e run testhad 8 pre-existing failures (fake-timer related, inprotocol:timeout:max-total/protocol:progress:callback/tools:call:progressscenarios) that I confirmed also fail identically on a cleanmaincheckout before my change — unrelated to this fix.check:skills/sync:snippets --checkpass; 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()inprotocol.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.