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
2 changed files with 127 additions and 19 deletions

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