Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-lions-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modelcontextprotocol/node": patch
---

Prevent Hono from overriding global Response object by passing `overrideGlobalObjects: false` to `getRequestListener()`. This fixes compatibility with frameworks like Next.js whose response classes extend the native Response.
38 changes: 24 additions & 14 deletions packages/middleware/node/src/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,19 @@ export class NodeStreamableHTTPServerTransport implements Transport {

// Create a request listener that wraps the web standard transport
// getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming
this._requestListener = getRequestListener(async (webRequest: Request) => {
// Get context if available (set during handleRequest)
const context = this._requestContext.get(webRequest);
return this._webStandardTransport.handleRequest(webRequest, {
authInfo: context?.authInfo,
parsedBody: context?.parsedBody
});
});
// overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
// break frameworks like Next.js whose response classes extend the native Response
this._requestListener = getRequestListener(
async (webRequest: Request) => {
// Get context if available (set during handleRequest)
const context = this._requestContext.get(webRequest);
return this._webStandardTransport.handleRequest(webRequest, {
authInfo: context?.authInfo,
parsedBody: context?.parsedBody
});
},
{ overrideGlobalObjects: false }
);
}

/**
Expand Down Expand Up @@ -157,12 +162,17 @@ export class NodeStreamableHTTPServerTransport implements Transport {
const authInfo = req.auth;

// Create a custom handler that includes our context
const handler = getRequestListener(async (webRequest: Request) => {
return this._webStandardTransport.handleRequest(webRequest, {
authInfo,
parsedBody
});
});
// overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
// break frameworks like Next.js whose response classes extend the native Response
const handler = getRequestListener(
async (webRequest: Request) => {
return this._webStandardTransport.handleRequest(webRequest, {
authInfo,
parsedBody
});
},
{ overrideGlobalObjects: false }
);

// Delegate to the request listener which handles all the Node.js <-> Web Standard conversion
// including proper SSE streaming support
Expand Down
83 changes: 83 additions & 0 deletions packages/middleware/node/test/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2932,6 +2932,89 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
});
});

describe('NodeStreamableHTTPServerTransport global Response preservation', () => {
it('should not override the global Response object', () => {
// Store reference to the original global Response constructor
const OriginalResponse = globalThis.Response;

// Create a custom class that extends Response (similar to Next.js's NextResponse)
class CustomResponse extends Response {
customProperty = 'test';
}

// Verify instanceof works before creating transport
const customResponseBefore = new CustomResponse('test body');
expect(customResponseBefore instanceof Response).toBe(true);
expect(customResponseBefore instanceof OriginalResponse).toBe(true);

// Create the transport - this should NOT override globalThis.Response
const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});

// Verify the global Response is still the original
expect(globalThis.Response).toBe(OriginalResponse);

// Verify instanceof still works after creating transport
const customResponseAfter = new CustomResponse('test body');
expect(customResponseAfter instanceof Response).toBe(true);
expect(customResponseAfter instanceof OriginalResponse).toBe(true);

// Verify that instances created before transport initialization still work
expect(customResponseBefore instanceof Response).toBe(true);

// Clean up
transport.close();
});

it('should not override the global Response object when calling handleRequest', async () => {
// Store reference to the original global Response constructor
const OriginalResponse = globalThis.Response;

// Create a custom class that extends Response
class CustomResponse extends Response {
customProperty = 'test';
}

const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});

// Create a mock server to test handleRequest
const port = await getFreePort();
const httpServer = createServer(async (req, res) => {
await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res);
});

await new Promise<void>(resolve => {
httpServer.listen(port, () => resolve());
});

try {
// Make a request to trigger handleRequest
await fetch(`http://localhost:${port}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream'
},
body: JSON.stringify(TEST_MESSAGES.initialize)
});

// Verify the global Response is still the original after handleRequest
expect(globalThis.Response).toBe(OriginalResponse);

// Verify instanceof still works
const customResponse = new CustomResponse('test body');
expect(customResponse instanceof Response).toBe(true);
expect(customResponse instanceof OriginalResponse).toBe(true);
} finally {
await transport.close();
httpServer.close();
}
});
});

/**
* Helper to create test server with DNS rebinding protection options
*/
Expand Down
18 changes: 14 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ catalogs:
eventsource-parser: ^3.0.0
jose: ^6.1.1
runtimeServerOnly:
'@hono/node-server': ^1.19.7
'@hono/node-server': ^1.19.8
content-type: ^1.0.5
cors: ^2.8.5
express: ^5.2.1
Expand Down
Loading