Skip to content

fix(mcp): HTTP transport responds with 500 on handler error instead of hanging#79

Merged
SingleSourceStudios merged 2 commits into
mainfrom
fix/mcp-http-error-response
May 26, 2026
Merged

fix(mcp): HTTP transport responds with 500 on handler error instead of hanging#79
SingleSourceStudios merged 2 commits into
mainfrom
fix/mcp-http-error-response

Conversation

@SingleSourceStudios
Copy link
Copy Markdown
Collaborator

@SingleSourceStudios SingleSourceStudios commented May 26, 2026

Summary

The MCP HTTP transport's request handler caught handleRequest rejections, wrote to stderr, and never responded. The client hung until its socket-level timeout, and the MCP host (Claude Desktop, Cursor, etc.) reported the server as unresponsive rather than surfacing an actual error. The stdio transport does not share this path and is untouched.

Fix

In packages/mcp/src/transport/http.ts, the .catch now:

  • logs the failure (using err.message when err is an Error),
  • if !res.headersSent: sets statusCode = 500, Content-Type: application/json, and ends with { error, detail },
  • else if !res.writableEnded: ends the response.

The headersSent / writableEnded guards prevent a double-write when the transport already started a response.

The request listener is extracted into a named createRequestListener(transport) factory (behaviour otherwise unchanged) so the error path can be exercised in isolation.

Test (TDD, RED first)

Added an integration test in packages/mcp/tests/server.test.mjs that imports createRequestListener from source, wires it to a stub transport whose handleRequest rejects, starts a real http.Server, POSTs a body, and asserts the client receives status 500 plus a JSON { error, detail } body within a 2s timeout.

Observed RED before the fix:

not ok 1 - responds 500 with a structured JSON body when handleRequest rejects
  error: 'request timed out after 2000ms (hung connection)'

GREEN after the fix (mcp suite: 19 pass, 0 fail).

Why the test injects a rejecting transport rather than POSTing a malformed body

The issue suggested POSTing a malformed JSON-RPC body. Empirically, on the installed SDK (@modelcontextprotocol/sdk 1.29.0) handleRequest delegates to Hono's request listener, which returns its own structured 400 for malformed JSON, bad Accept, missing session, and truncated bodies. None of those reach the .catch, so a malformed-body test passes before and after the fix and proves nothing. The test therefore drives the real rejection path through an injected transport, which is genuinely RED before the change.

Test imports from TypeScript source

The test imports the factory from ../src/transport/http.ts. Node 22.18+ strips TypeScript types on import by default (confirmed on 22.22.2), so this needs no build-config or test-runner change. tsup.config.ts was not touched.

Verification (node v22.22.2)

  • npm run build:core and full npm run build: success
  • npm test (root vitest): 473 passed, 0 failed
  • mcp node --test: 19 pass, 0 fail (includes the new bug(mcp): HTTP transport logs but never responds to client on handler error #66 test)
  • npm run typecheck (root): clean
  • npm run lint: both changed files clean (repo baseline of 5 errors / 22 warnings is in untouched files)
  • node spec/fixtures/run-fixtures.mjs: 29 passed, 0 failed

Note: the mcp package's standalone tsc --noEmit reports 44 errors (missing node globals), but this is pre-existing on main (identical count with and without this change) and stems from the mcp package's tsconfig not resolving @types/node; it is unrelated to this fix and not among the project gates.

Closes #66


Summary by cubic

Fixes the MCP HTTP transport so handler failures return a 500 JSON error instead of hanging, letting clients see a clear error and hosts avoid marking the server as unresponsive.

  • Bug Fixes

    • Return 500 with application/json body { error, detail } when handleRequest rejects.
    • Guard with res.headersSent and res.writableEnded; log the error to stderr.
    • Added an integration test that confirms the 500 JSON response instead of a hang.
  • Refactors

    • Extracted createRequestListener(transport) to isolate and test the error path; behavior unchanged and stdio transport untouched.

Written for commit 2f7606e. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • HTTP transport now properly handles errors during request processing, returning a 500 status with a structured JSON error response containing error details. Prevents server hangs when request handling fails.

Review Change Stack

The HTTP transport's request handler caught handleRequest rejections, wrote
to stderr, and never responded. The client hung until its socket timeout and
the MCP host reported the server as unresponsive rather than surfacing an
error.

The .catch now logs (using err.message for Error instances), then sends a
structured response: status 500, Content-Type application/json, and a body
of {error, detail} when headers have not been sent. If the transport already
started a response, it ends the response when it is still writable. The
headersSent and writableEnded guards prevent a double-write.

The request listener is extracted into createRequestListener(transport) so the
error path can be tested with an injected transport. The added integration
test imports that factory from source, wires it to a transport whose
handleRequest rejects, and asserts the client receives 500 plus a JSON body
within a timeout. Before the fix that request hangs and the test fails on the
timeout; after, it returns the structured error. The stdio transport is
untouched.

A malformed JSON-RPC body does not reach this path on the installed SDK
(@modelcontextprotocol/sdk 1.29.0 returns its own 400 via Hono before the
catch), so the test drives the actual rejection path through the injected
transport rather than a malformed body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

The HTTP transport previously logged rejections to stderr but never sent responses to clients, leaving them hanging. This change extracts request handling into a reusable createRequestListener function that catches errors, logs them, and returns either a 500 JSON response or safely ends the response. The server is updated to use this listener, and integration tests validate the error path prevents hangs.

Changes

HTTP transport error response handling

Layer / File(s) Summary
Error-handling request listener
packages/mcp/src/transport/http.ts
createRequestListener(transport) catches transport.handleRequest rejections, logs to stderr, and returns either a structured 500 JSON response (when headers aren't sent) or ends the response to prevent hangs. startHttp is updated to use this new listener when creating the HTTP server.
Integration test for error responses
packages/mcp/tests/server.test.mjs
Import createRequestListener and add a test suite that spins up an HTTP server with a rejecting stub transport, posts a JSON-RPC request, uses postWithTimeout to detect hangs, and asserts the server responds with HTTP 500 and structured JSON (error and detail fields).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

A listener once silent when errors did creep,
Now speaks with a 500 and secrets to keep.
No more do the clients hang, waiting alone—
The rabbit's response ensures they go home! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: HTTP transport now responds with 500 on handler error instead of hanging.
Linked Issues check ✅ Passed The PR fully addresses all requirements from issue #66: sends 500 with JSON error body, uses headersSent/writableEnded guards, logs errors properly, and includes an integration test with rejecting transport injection.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the HTTP transport error handling and adding related tests; no unrelated modifications are present.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering the bug, fix, test approach, and verification results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mcp-http-error-response

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Re-trigger cubic

@SingleSourceStudios SingleSourceStudios enabled auto-merge (squash) May 26, 2026 22:18
@SingleSourceStudios SingleSourceStudios merged commit 42425fe into main May 26, 2026
4 checks passed
@SingleSourceStudios SingleSourceStudios deleted the fix/mcp-http-error-response branch May 26, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(mcp): HTTP transport logs but never responds to client on handler error

1 participant