Skip to content

Commit 6e8f7e1

Browse files
authored
fix: prevent Hono from overriding global Response object (v1.x) (#1411)
1 parent 12ae856 commit 6e8f7e1

File tree

4 files changed

+122
-19
lines changed

4 files changed

+122
-19
lines changed

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"client": "tsx scripts/cli.ts client"
8787
},
8888
"dependencies": {
89-
"@hono/node-server": "^1.19.7",
89+
"@hono/node-server": "^1.19.9",
9090
"ajv": "^8.17.1",
9191
"ajv-formats": "^3.0.1",
9292
"content-type": "^1.0.5",

src/server/streamableHttp.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,19 @@ export class StreamableHTTPServerTransport implements Transport {
7878

7979
// Create a request listener that wraps the web standard transport
8080
// getRequestListener converts Node.js HTTP to Web Standard and properly handles SSE streaming
81-
this._requestListener = getRequestListener(async (webRequest: Request) => {
82-
// Get context if available (set during handleRequest)
83-
const context = this._requestContext.get(webRequest);
84-
return this._webStandardTransport.handleRequest(webRequest, {
85-
authInfo: context?.authInfo,
86-
parsedBody: context?.parsedBody
87-
});
88-
});
81+
// overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
82+
// break frameworks like Next.js whose response classes extend the native Response
83+
this._requestListener = getRequestListener(
84+
async (webRequest: Request) => {
85+
// Get context if available (set during handleRequest)
86+
const context = this._requestContext.get(webRequest);
87+
return this._webStandardTransport.handleRequest(webRequest, {
88+
authInfo: context?.authInfo,
89+
parsedBody: context?.parsedBody
90+
});
91+
},
92+
{ overrideGlobalObjects: false }
93+
);
8994
}
9095

9196
/**
@@ -166,12 +171,17 @@ export class StreamableHTTPServerTransport implements Transport {
166171
const authInfo = req.auth;
167172

168173
// Create a custom handler that includes our context
169-
const handler = getRequestListener(async (webRequest: Request) => {
170-
return this._webStandardTransport.handleRequest(webRequest, {
171-
authInfo,
172-
parsedBody
173-
});
174-
});
174+
// overrideGlobalObjects: false prevents Hono from overwriting global Response, which would
175+
// break frameworks like Next.js whose response classes extend the native Response
176+
const handler = getRequestListener(
177+
async (webRequest: Request) => {
178+
return this._webStandardTransport.handleRequest(webRequest, {
179+
authInfo,
180+
parsedBody
181+
});
182+
},
183+
{ overrideGlobalObjects: false }
184+
);
175185

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

test/server/streamableHttp.test.ts

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

2913+
describe('StreamableHTTPServerTransport global Response preservation', () => {
2914+
it('should not override the global Response object', () => {
2915+
// Store reference to the original global Response constructor
2916+
const OriginalResponse = globalThis.Response;
2917+
2918+
// Create a custom class that extends Response (similar to Next.js's NextResponse)
2919+
class CustomResponse extends Response {
2920+
customProperty = 'test';
2921+
}
2922+
2923+
// Verify instanceof works before creating transport
2924+
const customResponseBefore = new CustomResponse('test body');
2925+
expect(customResponseBefore instanceof Response).toBe(true);
2926+
expect(customResponseBefore instanceof OriginalResponse).toBe(true);
2927+
2928+
// Create the transport - this should NOT override globalThis.Response
2929+
const transport = new StreamableHTTPServerTransport({
2930+
sessionIdGenerator: () => randomUUID()
2931+
});
2932+
2933+
// Verify the global Response is still the original
2934+
expect(globalThis.Response).toBe(OriginalResponse);
2935+
2936+
// Verify instanceof still works after creating transport
2937+
const customResponseAfter = new CustomResponse('test body');
2938+
expect(customResponseAfter instanceof Response).toBe(true);
2939+
expect(customResponseAfter instanceof OriginalResponse).toBe(true);
2940+
2941+
// Verify that instances created before transport initialization still work
2942+
expect(customResponseBefore instanceof Response).toBe(true);
2943+
2944+
// Clean up
2945+
transport.close();
2946+
});
2947+
2948+
it('should not override the global Response object when calling handleRequest', async () => {
2949+
// Store reference to the original global Response constructor
2950+
const OriginalResponse = globalThis.Response;
2951+
2952+
// Create a custom class that extends Response
2953+
class CustomResponse extends Response {
2954+
customProperty = 'test';
2955+
}
2956+
2957+
const transport = new StreamableHTTPServerTransport({
2958+
sessionIdGenerator: () => randomUUID()
2959+
});
2960+
2961+
// Create a mock server to test handleRequest
2962+
const port = await getFreePort();
2963+
const httpServer = createServer(async (req, res) => {
2964+
await transport.handleRequest(req as IncomingMessage & { auth?: AuthInfo }, res);
2965+
});
2966+
2967+
await new Promise<void>(resolve => {
2968+
httpServer.listen(port, () => resolve());
2969+
});
2970+
2971+
try {
2972+
// Make a request to trigger handleRequest
2973+
await fetch(`http://localhost:${port}`, {
2974+
method: 'POST',
2975+
headers: {
2976+
'Content-Type': 'application/json',
2977+
Accept: 'application/json, text/event-stream'
2978+
},
2979+
body: JSON.stringify(TEST_MESSAGES.initialize)
2980+
});
2981+
2982+
// Verify the global Response is still the original after handleRequest
2983+
expect(globalThis.Response).toBe(OriginalResponse);
2984+
2985+
// Verify instanceof still works
2986+
const customResponse = new CustomResponse('test body');
2987+
expect(customResponse instanceof Response).toBe(true);
2988+
expect(customResponse instanceof OriginalResponse).toBe(true);
2989+
} finally {
2990+
await transport.close();
2991+
httpServer.close();
2992+
}
2993+
});
2994+
});
2995+
29132996
/**
29142997
* Helper to create test server with DNS rebinding protection options
29152998
*/

0 commit comments

Comments
 (0)