fix: prevent Hono from overriding global Response object (#1410)

This commit is contained in:
Matt 2026-01-20 12:17:19 +01:00 • committed by GitHub
commit 9296459ac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 127 additions and 19 deletions

View file

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

View file

@ -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 }
);
}
/**
@ -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

View file

@ -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
*/

18
pnpm-lock.yaml generated
View file

@ -87,8 +87,8 @@ catalogs:
version: 6.1.3
runtimeServerOnly:
'@hono/node-server':
specifier: ^1.19.7
version: 1.19.7
specifier: ^1.19.8
version: 1.19.8
cors:
specifier: ^2.8.5
version: 2.8.5
@ -310,7 +310,7 @@ importers:
dependencies:
'@hono/node-server':
specifier: catalog:runtimeServerOnly
version: 1.19.7(hono@4.11.3)
version: 1.19.8(hono@4.11.3)
'@modelcontextprotocol/examples-shared':
specifier: workspace:^
version: link:../shared
@ -700,7 +700,7 @@ importers:
dependencies:
'@hono/node-server':
specifier: catalog:runtimeServerOnly
version: 1.19.7(hono@4.11.3)
version: 1.19.8(hono@4.11.3)
devDependencies:
'@eslint/js':
specifier: catalog:devTools
@ -1211,6 +1211,12 @@ packages:
peerDependencies:
hono: ^4
'@hono/node-server@1.19.8':
resolution: {integrity: sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -4225,6 +4231,10 @@ snapshots:
dependencies:
hono: 4.11.3
'@hono/node-server@1.19.8(hono@4.11.3)':
dependencies:
hono: 4.11.3
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':

View file

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