Skip to content

fix(core): keep the connection up when a request POST is HTTP-refused - #2777

Merged
cliffhall merged 1 commit into
v2/mainfrom
v2/fix/2776-listen-refusal-node-onerror
Oct 21, 2026
Merged

cliffhall merged 1 commit into
v2/mainfrom
v2/fix/2776-listen-refusal-node-onerror

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2776

Problem

The SDK's StreamableHTTPClientTransport._send reports a failed POST through onerror and then rethrows it to the sender. InspectorClient's baseTransport.onerror treated every report as a failed connection and set the status to error.

So on the Node transport, any request the server refused with a bare HTTP status took the whole session down, even though the request's own rejection already surfaces it. For #2722 that meant the TUI's new Listen stream: refused by the server could not be reached: the connect-time listen 403 errored the client before the Subscriptions tab could show. PR #2730 called this out as pre-existing and out of scope, and the v2.12.0 smoke (#2764) recorded it as partial.

Change

core/mcp/inspectorClient.ts: onerror returns early, leaving the status alone, when subscriptionListenRefusal(error) classifies the error as a deterministic HTTP refusal: an SdkHttpError with ClientHttpNotImplemented and status 400, 403, 404 or 405. The SDK throws that code only from its POST path, so the error is always also delivered to the sender as a rejection. Everything else reported through onerror still errors the connection, as before: an SSE GET stream failing, a dropped connection, a 5xx, an auth failure. The web client is unaffected, because its remote transport doesn't route send failures through onerror.

Tests

  • Unit (inspectorClient-peer-handler-timing.test.ts, new transport onerror (#2776) block): onerror with a POST refusal at 400, 403, 404 or 405 keeps the client connected and dispatches no error event. A 503 POST failure, an auth failure and a plain transport Error still set error and dispatch it.
  • Integration (inspectorClient-subscriptions-era.test.ts, against the real refusing server): the bare-403 subscribe and the connect-time listen cases now also assert getStatus() is connected, and the connect-time case asserts the stored refusal. The comment above the stubbed block is updated.
  • Mutation check: with the guard removed, exactly the new assertions fail, 4 unit and 2 integration.

Verified by hand

TUI in a real pty (160×50) against subscriptions-listen-refused-http.json on the modern era, keys c (connect) then u (Subscriptions):

This branch Published 2.11.0
Header ● connected ✗ error
Tabs Resources (3) ▶ Subscriptions (0) … Protocol (4) no Subscriptions tab, Resources (0)
Pane Not subscribed / Listen stream: refused by the server Error / Error POSTing to endpoint: Forbidden
15 s later unchanged, Protocol (4) (nothing re-sent) unchanged

npm run local:gate: green (exit 0).

🤖 Generated with Claude Code

The SDK's StreamableHTTP _send reports a failed POST through onerror and
then rethrows it to the sender. InspectorClient's onerror errored the
whole connection, so on the Node transport a refused subscriptions/listen
took the session down before #2722's refused stream status could show,
and the TUI's 'Listen stream: refused by the server' was unreachable.

A deterministic HTTP refusal (the #2722 classifier: ClientHttpNotImplemented
with 400/403/404/405, a code the SDK throws only from its POST path) now
leaves the status alone; the request's rejection already surfaces it.
Everything else reported through onerror still errors the connection.

Closes #2776

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: cliffhall <[email protected]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The narrow guard matches the requested behavior and has focused regression coverage with no unresolved findings.

No open issues 🎉

What changed in this PR

Keeps Node transport sessions connected when a request POST receives a deterministic HTTP refusal, allowing subscription refusal notices to remain visible.

Changes:

  • Reuses the existing refusal classifier without changing handling of other transport errors.
  • Adds unit and integration assertions for connection status and refusal reporting.
File Description
core/​mcp/​inspectorClient.ts Ignores request-level HTTP refusals in the connection error handler.
clients/​web/​src/​test/​integration/​mcp/​inspectorClient-subscriptions-era.test.ts Verifies refused subscriptions leave real sessions connected.
clients/​web/​src/​test/​core/​mcp/​inspectorClient-peer-handler-timing.test.ts Tests refusal statuses and continued handling of fatal errors.

Review effort: Balanced

@cliffhall
cliffhall merged commit 3cdde6c into v2/main Oct 21, 2026
11 checks passed
@cliffhall
cliffhall deleted the v2/fix/2776-listen-refusal-node-onerror branch October 21, 2026 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Node transport: an HTTP-refused request POST errors the whole connection (TUI never shows the #2722 refusal)

2 participants