Skip to content

fix(server): fire onsessionclosed once when DELETEs overlap (v1.x) - #2587

Draft
axits-lab wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
axits-lab:fix/v1x-delete-race
Draft

fix(server): fire onsessionclosed once when DELETEs overlap (v1.x)#2587
axits-lab wants to merge 2 commits into
modelcontextprotocol:v1.xfrom
axits-lab:fix/v1x-delete-race

Conversation

@axits-lab

Copy link
Copy Markdown

v1.x backport of #2583. Refs #2562 (item 2), which notes the item applies to both branches — I offered this on the issue and confirmed the window is present here before writing anything.

Draft: non-write-access users are capped at one open non-draft PR on this repo and #2582 holds it. Ready for review as-is.

The race, on this branch

WebStandardStreamableHTTPServerTransport.handleDeleteRequest is byte-identical to main's pre-fix code:

try {
    await Promise.resolve(this._onsessionclosed?.(this.sessionId!));
    return new Response(null, { status: 200 });
} finally {
    await this.close();
}

_closed is set by close(), so it stays false for as long as the callback is in flight. Neither validateSession nor validateProtocolVersion consults it, so a second DELETE arriving in that window passes every guard and fires the callback again for a session already being torn down.

Fix

Claim the notification synchronously, before the await — _closed cannot serve the purpose since by construction it is only set after the callback settles. DELETE stays idempotent: a concurrent or repeat request still terminates the session and answers 200, it just does not re-run the callback.

Test — one note worth flagging

The regression test drives transport.handleRequest() directly rather than going through a real socket.

My first attempt used fetch against createTestServer, matching the surrounding onsessionclosed tests, and it passed without the fix — over HTTP the callback release wins the race, so the two DELETEs simply serialize and the bug never surfaces. Driving the transport in-process makes the second DELETE provably reach the handler while the first is parked. It follows the existing WebStandardStreamableHTTPServerTransport - onerror callback block, which already tests this way.

Verified it fails without the src change:

× fires onsessionclosed once when two DELETEs overlap

Verification

  • 1640/1640 tests pass across 52 files
  • npm run lint and npm run typecheck clean

Changeset included.

Backport of the main-branch fix (modelcontextprotocol#2583) to v1.x, where the same window
exists in `WebStandardStreamableHTTPServerTransport`.

`handleDeleteRequest` awaited `onsessionclosed` before `close()` ran, so
`_closed` was still false while the callback was in flight. A second
DELETE for the same session passed `validateSession`/`validateProtocolVersion`
unchanged — neither consults `_closed` — and invoked the callback again for
a session already being torn down.

Claim the notification synchronously before the await. `_closed` cannot
serve this purpose: it is set by `close()`, which only runs once the
callback has settled.

DELETE stays idempotent — a concurrent or repeat request still terminates
the session and answers 200, it just does not re-run the callback.

The regression test drives `handleRequest()` directly rather than a real
socket: over HTTP the callback release wins the race, the two DELETEs
serialize, and the bug does not reproduce.

Refs modelcontextprotocol#2562 (item 2)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1312370

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

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 Jul 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@2587

commit: 1312370

`prettier --check .` covers .changeset/*.md on this branch; the file was
written by hand and never formatted, which failed the build job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@axits-lab axits-lab closed this Jul 30, 2026
@axits-lab axits-lab reopened this Jul 30, 2026
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.

1 participant