Skip to content

fix(sse): keep hook headers on the legacy SSE GET response - #349

Open
mcollina wants to merge 1 commit into
mainfrom
fix/335-sse-hook-headers
Open

mcollina wants to merge 1 commit into
mainfrom
fix/335-sse-hook-headers

Conversation

@mcollina

Copy link
Copy Markdown
Member

Problem

The legacy GET /mcp SSE handler calls reply.hijack() and then raw.writeHead(200). Headers that hooks put on the reply (CORS, security headers) are dropped. A cross-origin browser client can neither open the stream nor read Mcp-Session-Id.

While writing the test I found a second bug on the same path: the error branch calls reply.send() after the hijack. Fastify ignores that, so a failure while opening the stream (the session store being down, for example) never got a response, and the client hung until it timed out.

Fix

  • Read reply.getHeaders() before hijack() and set them on the raw response. Then set the SSE headers on top, so Content-Type and Cache-Control still win. This follows what the 2026-07-28 streams do on feat: implement MCP 2026-07-28 alongside the handshake revisions #170.
  • On the hijacked error path, write a 500 JSON response on the raw response. It keeps the hook headers and switches Content-Type to application/json. If the head has already been sent, the stream is destroyed instead. Errors raised before the hijack still go through reply.send() as before.

onSend hooks still do not run for this response, as with any hijacked reply. Headers have to be set in onRequest/preHandler, which is where CORS plugins set them.

Tests

test/sse-hook-headers.test.ts:

  • the 200 stream response carries the headers set by an onRequest hook, next to text/event-stream and Mcp-Session-Id
  • with sessionStore.create failing, a real listening server answers 500 with JSON and the hook headers. Before this fix it timed out.

Fixes #335

🤖 Generated with Claude Code

https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9

The handler hijacked the reply and wrote the head straight to the raw
response, so headers set by hooks (CORS, security headers) were lost
and a cross-origin client could neither open the stream nor read
Mcp-Session-Id. Copy reply.getHeaders() onto the raw response right
after hijacking.

The error path called reply.send() after the hijack, which Fastify
ignores, so a failure while opening the stream never answered. Write
the 500 on the raw response instead, with the same headers, or destroy
the stream if its head has already gone out.

Fixes #335

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LmhsFjk8W96X2PvmShmDm9

This branch has not been deployed

No deployments
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.

Legacy SSE GET drops headers set by hooks (CORS, security headers)

1 participant