fix: prevent Hono from overriding global Response object (#1410)
This commit is contained in:
parent
f66a55b5f4
commit
9296459ac0
5 changed files with 127 additions and 19 deletions
5
.changeset/brave-lions-glow.md
Normal file
5
.changeset/brave-lions-glow.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
18
pnpm-lock.yaml
generated
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue