Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix: call onerror callback for all error responses in StreamableHTTPServerTransport by kdn-posipaka · Pull Request #1687 · modelcontextprotocol/typescript-sdk · GitHub
Skip to content

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

Closed
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses
Closed

fix: call onerror callback for all error responses in StreamableHTTPServerTransport#1687
kdn-posipaka wants to merge 6 commits into
modelcontextprotocol:mainfrom
kdn-posipaka:fix/onerror-callback-for-all-error-responses

Conversation

@kdn-posipaka

Copy link
Copy Markdown

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.
Fixesmodelcontextprotocol#1395
@changeset-bot

changeset-botBot commented Mar 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5fcc3b1

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-newBot commented Mar 16, 2026

Copy link
Copy Markdown

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: 5fcc3b1

kai-agent-free added 2 commits March 16, 2026 13:30
- Add non-null assertions for errors[0] access after length check (TS2532)
- Remove unused catch binding in streamableHttp.ts
- Format with prettier

@travisbreakstravisbreaks 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.

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:

privatecreateJsonErrorResponse(status: number,code: number,message: string,options?: {headers?: Record<string,string>;data?: string;cause?: Error}): Response{this.onerror?.(options?.cause??newError(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
@kdn-posipaka

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

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.

@kdn-posipaka

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

@Maverick-666

Copy link
Copy Markdown

Thanks! I can take this.

I’ll add focused regression coverage for:

  1. handleUnsupportedRequest path triggers onerror
  2. centralized error-response path preserves error fidelity via cause
  3. parse-failure paths still surface meaningful original error context to onerror

I’ll keep it test-only and minimal, then share the commit/PR link here.

@Maverick-666

Copy link
Copy Markdown

Quick update: I implemented the test follow-up, but I don’t have push permission to kai-agent-free/typescript-sdk (got 403 when pushing to the PR head branch).

I pushed the changes to my fork instead:

  • Branch: Maverick-666:codex/pr1687-onerror-tests
  • Commit: 65e3de3 (test(server): add regression coverage for onerror unsupported-method and cause fidelity)

Local verification:

  • pnpm --filter @modelcontextprotocol/server test -- test/server/streamableHttp.test.ts (pass)

If convenient, you can cherry-pick this commit:

git fetch https://github.com/Maverick-666/typescript-sdk.git codex/pr1687-onerror-tests
git cherry-pick 65e3de3

@kdn-posipaka

Copy link
Copy Markdown
Author

Thanks @Maverick-666 for the additional test coverage! 🙏 I've cherry-picked your commit (65e3de3) into this branch — the unsupported-method onerror regression test and cause fidelity check are great additions. All 385 tests passing.

@Maverick-666

Copy link
Copy Markdown

Awesome, thanks for cherry-picking and confirming all tests pass.

Glad this helped strengthen the PR. Happy to help with any additional follow-ups if needed.

@felixweinberger

Copy link
Copy Markdown
Contributor

Thanks for the contribution! Closing in favor of #1433.

Sign up for freeto 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

4 participants

@kdn-posipaka@Maverick-666@felixweinberger@travisbreaks