Skip to content

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687

Open
kai-agent-free wants to merge 4 commits intomodelcontextprotocol:mainfrom
kai-agent-free:fix/onerror-callback-for-all-error-responses
Open

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kai-agent-free wants to merge 4 commits intomodelcontextprotocol:mainfrom
kai-agent-free:fix/onerror-callback-for-all-error-responses

Conversation

@kai-agent-free
Copy link

Summary

Fixes #1395

Several error paths in StreamableHTTPServerTransport returned JSON error responses via createJsonErrorResponse() without calling the onerror callback, silently swallowing transport errors.

Changes

packages/server/src/server/streamableHttp.ts:

  • Added this.onerror?.(new Error(message)) inside createJsonErrorResponse() so every error response automatically triggers the callback
  • Removed 4 redundant manual this.onerror?.() calls that preceded createJsonErrorResponse() to avoid double-firing

This ensures all error types are observable — including parse errors, invalid headers, session validation failures, and protocol version mismatches — which were previously invisible to users.

packages/server/test/server/streamableHttp.test.ts:

  • Added 5 new tests verifying onerror is called for: invalid Accept header (POST & GET), invalid Content-Type, invalid JSON, and invalid JSON-RPC messages

Test Results

All 42 tests pass (37 existing + 5 new). TypeScript compiles cleanly.

…erverTransport

Previously, many error paths in handlePostRequest, handleGetRequest, and
handleDeleteRequest would return JSON error responses without calling the
onerror callback, making these errors invisible to users who set up error
logging via transport.onerror.

This fix moves the onerror call into createJsonErrorResponse itself, ensuring
every error response consistently triggers the callback. Redundant manual
onerror calls before createJsonErrorResponse are removed to avoid double-firing.

Fixes modelcontextprotocol#1395
@kai-agent-free kai-agent-free requested a review from a team as a code owner March 16, 2026 12:35
@changeset-bot
Copy link

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: 3a1a8b7

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
Copy link

pkg-pr-new bot commented Mar 16, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1687

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1687

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1687

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1687

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1687

commit: 3a1a8b7

- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier
Copy link

@travisbreaks travisbreaks left a comment

Choose a reason for hiding this comment

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

Good change overall. Centralizing onerror in createJsonErrorResponse() is the right structural call: it guarantees coverage for every path that uses that helper, including validateSession, validateProtocolVersion, and the various 4xx checks that were previously silent. The test coverage is solid.

A few things worth flagging:

1. handleUnsupportedRequest() bypasses the centralized path

handleUnsupportedRequest() (the 405 handler for PUT/PATCH/etc.) builds its own Response.json(...) directly instead of calling createJsonErrorResponse(). After this PR, that error path still won't fire onerror. It may be intentional (unsupported HTTP methods aren't really transport errors), but if the goal is "every error response triggers the callback," this is a gap. Worth either converting it to use createJsonErrorResponse() or documenting the exception.

2. Lost error fidelity in catch blocks

The two catch blocks that previously called onerror with the actual caught error (error as Error) now get new Error(message) where message is a generic string like "Error replaying events" or "Parse error". The real exception (with its original message, type, and stack trace) is discarded from the callback. For the handlePostRequest outer catch, the original error still appears as data in the HTTP response body, but anyone relying on onerror for logging/observability loses the underlying cause.

Consider preserving the original error when one is available. For example, the replayEvents catch could pass the caught error to onerror and still call createJsonErrorResponse separately (accepting the "double call" in that one path), or createJsonErrorResponse could accept an optional cause parameter:

private createJsonErrorResponse(
    status: number,
    code: number,
    message: string,
    options?: { headers?: Record<string, string>; data?: string; cause?: Error }
): Response {
    this.onerror?.(options?.cause ?? new Error(message));
    // ...
}

3. Overlap with #1684

PR #1684 fixes a subset of the same issue (the two inner parse catch blocks in handlePostRequest). That PR takes a different approach: it adds targeted onerror calls at each catch site and preserves the actual caught error object. If this PR lands first, #1684 becomes a no-op for those paths. If #1684 lands first, this PR would need a rebase and could potentially double-fire on those two catch blocks. The maintainers should pick one direction and close the other.

Between the two, this PR has better architectural coverage (all createJsonErrorResponse callers get onerror for free going forward). But #1684 preserves error fidelity. The ideal outcome would be this PR's centralized approach combined with #1684's pattern of forwarding the real error object.

Nice tests. The changeset is included, which #1684 is missing.

…n and error cause preservation

- Convert handleUnsupportedRequest() to use createJsonErrorResponse() for consistency
- Add optional 'cause' parameter to createJsonErrorResponse() to preserve error fidelity
- Pass caught errors as 'cause' in catch blocks to maintain original error information
- Add test for onerror callback on unsupported HTTP methods
@kai-agent-free
Copy link
Author

Thanks for the thorough review, @travisbreaks! Great catches — I've pushed a commit addressing your feedback:

  1. handleUnsupportedRequest() now uses createJsonErrorResponse() — fully consistent with the centralized path. The Allow header is passed via the headers option.

  2. Error fidelity preserved via cause parameter — Added an optional cause?: Error field to the options. The onerror callback now receives options?.cause ?? new Error(message), so catch blocks that have the original error can pass it through (e.g., the POST parse error catch block now passes the caught error as cause).

  3. Overlap with fix(server): surface parse/validation transport errors via onerror in streamable HTTP #1684 — This PR supersedes fix(server): surface parse/validation transport errors via onerror in streamable HTTP #1684's scope (centralizing onerror in createJsonErrorResponse covers what fix(server): surface parse/validation transport errors via onerror in streamable HTTP #1684 aimed to fix). This one should land first; fix(server): surface parse/validation transport errors via onerror in streamable HTTP #1684 can then be closed or rebased if needed.

All tests pass (43/43). Let me know if anything else needs adjustment!

@Maverick-666
Copy link

Thanks @travisbreaks and @kai-agent-free — great progress on this.

I closed #1684 to consolidate on this PR and avoid parallel fixes for #1395. I’m happy to actively help push this over the line.

If useful, I can immediately contribute one focused follow-up commit that:

  1. adds/expands regression tests for the remaining centralized error paths,
  2. verifies original error fidelity (cause) is preserved end-to-end in onerror,
  3. updates any related docs/changelog bits if maintainers want that included here.

If you’re good with that scope, I can open the follow-up against this branch today.

@kai-agent-free
Copy link
Author

Thanks @Maverick-666! Appreciate closing #1684 to consolidate — makes sense to avoid parallel fixes.

Your follow-up commit proposal sounds good. The regression test coverage for the new cause parameter + handleUnsupportedRequest path would strengthen the PR. Feel free to push to the branch if you'd like, or I can incorporate specific test cases you have in mind.

The handlePostRequest outer catch already passes the caught error as cause now, so the fidelity issue travisbreaks flagged should be resolved. Happy to iterate on anything else.

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.

Some transport errors are silently swallowed due to missing onerror callback usage

3 participants