Skip to content

Commit 9296459

Browse files
authored
fix: prevent Hono from overriding global Response object (modelcontextprotocol#1410)
1 parent f66a55b commit 9296459

File tree

5 files changed

+127
-19
lines changed

5 files changed

+127
-19
lines changed

.changeset/brave-lions-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@modelcontextprotocol/node": patch
3+
---
4+
5+
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.

packages/middleware/node/src/streamableHttp.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,19 @@ export class NodeStreamableHTTPServerTransport implements Transport {
6969

7070
// Create a request listener that wraps the web standard transport
7171
// getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming
72-
this._requestListener = getRequestListener(async (webRequest: Request) => {
73-
// Get context if available (set during handleRequest)
74-
const context = this._requestContext.get(webRequest);
75-
return this._webStandardTransport.handleRequest(webRequest, {
76-
authInfo: context?.authInfo,
77-
parsedBody: context?.parsedBody
78-
});
79-
});
72+
// overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
73+
// break frameworks like Next.js whose response classes extend the native Response
74+
this._requestListener = getRequestListener(
75+
async (webRequest: Request) => {
76+
// Get context if available (set during handleRequest)
77+
const context = this._requestContext.get(webRequest);
78+
return this._webStandardTransport.handleRequest(webRequest, {
79+
authInfo: context?.authInfo,
80+
parsedBody: context?.parsedBody
81+
});
82+
},
83+
{ overrideGlobalObjects: false }
84+
);
8085
}
8186

8287
/**
@@ -157,12 +162,17 @@ export class NodeStreamableHTTPServerTransport implements Transport {
157162
const authInfo = req.auth;
158163

159164
// Create a custom handler that includes our context
160-
const handler = getRequestListener(async (webRequest: Request) => {
161-
return this._webStandardTransport.handleRequest(webRequest, {
162-
authInfo,
163-
parsedBody
164-
});
165-
});
165+
// overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
166+
// break frameworks like Next.js whose response classes extend the native Response
167+
const handler = getRequestListener(
168+
async (webRequest: Request) => {
169+
return this._webStandardTransport.handleRequest(webRequest, {
170+
authInfo,
171+
parsedBody
172+
});
173+
},
174+
{ overrideGlobalObjects: false }
175+
);
166176

167177
// Delegate to the request listener which handles all the Node.js <-> Web Standard conversion
168178
// including proper SSE streaming support

packages/middleware/node/test/streamableHttp.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2932,6 +2932,89 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
29322932
});
29332933
});
29342934

2935+
describe('NodeStreamableHTTPServerTransport global Response preservation', () => {
2936+
it('should not override the global Response object', () => {
2937+
// Store reference to the original global Response constructor
2938+
const OriginalResponse = globalThis.Response;
2939+
2940+
// Create a custom class that extends Response (similar to Next.js's NextResponse)
2941+
class CustomResponse extends Response {
2942+
customProperty = 'test';
2943+
}
2944+
2945+
// Verify instanceof works before creating transport
2946+
const customResponseBefore = new CustomResponse('test body');
2947+
expect(customResponseBefore instanceof Response).toBe(true);
2948+
expect(customResponseBefore instanceof OriginalResponse).toBe(true);
2949+
2950+
// Create the transport - this should NOT override globalThis.Response
2951+
const transport = new NodeStreamableHTTPServerTransport({
2952+
sessionIdGenerator: () => randomUUID()
2953+
});
2954+
2955+
// Verify the global Response is still the original
2956+
expect(globalThis.Response).toBe(OriginalResponse);
2957+
2958+
// Verify instanceof still works after creating transport
2959+
const customResponseAfter = new CustomResponse('test body');
2960+
expect(customResponseAfter instanceof Response).toBe(true);
2961+
expect(customResponseAfter instanceof OriginalResponse).toBe(true);
2962+
2963+
// Verify that instances created before transport initialization still work
2964+
expect(customResponseBefore instanceof Response).toBe(true);
2965+
2966+
// Clean up
2967+
transport.close();
2968+
});
2969+
2970+
it('should not override the global Response object when calling handleRequest', async () => {
2971+
// Store reference to the original global Response constructor
2972+
const OriginalResponse = globalThis.Response;
2973+
2974+
// Create a custom class that extends Response
2975+
class CustomResponse extends Response {
2976+
customProperty = 'test';
2977+
}
2978+
2979+
const transport = new NodeStreamableHTTPServerTransport({
2980+
sessionIdGenerator: () => randomUUID()
2981+
});
2982+
2983+
// Create a mock server to test handleRequest
2984+
const port = await getFreePort();
2985+
const httpServer = createServer(async (req, res) => {
2986+
await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res);
2987+
});
2988+
2989+
await new Promise<void>(resolve => {
2990+
httpServer.listen(port, () => resolve());
2991+
});
2992+
2993+
try {
2994+
// Make a request to trigger handleRequest
2995+
await fetch(`http://localhost:${port}`, {
2996+
method: 'POST',
2997+
headers: {
2998+
'Content-Type': 'application/json',
2999+
Accept: 'application/json, text/event-stream'
3000+
},
3001+
body: JSON.stringify(TEST_MESSAGES.initialize)
3002+
});
3003+
3004+
// Verify the global Response is still the original after handleRequest
3005+
expect(globalThis.Response).toBe(OriginalResponse);
3006+
3007+
// Verify instanceof still works
3008+
const customResponse = new CustomResponse('test body');
3009+
expect(customResponse instanceof Response).toBe(true);
3010+
expect(customResponse instanceof OriginalResponse).toBe(true);
3011+
} finally {
3012+
await transport.close();
3013+
httpServer.close();
3014+
}
3015+
});
3016+
});
3017+
29353018
/**
29363019
* Helper to create test server with DNS rebinding protection options
29373020
*/

pnpm-lock.yaml

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ catalogs:
3434
eventsource-parser: ^3.0.0
3535
jose: ^6.1.1
3636
runtimeServerOnly:
37-
'@hono/node-server': ^1.19.7
37+
'@hono/node-server': ^1.19.8
3838
content-type: ^1.0.5
3939
cors: ^2.8.5
4040
express: ^5.2.1

0 commit comments

Comments
 (0)