fix: prevent Hono from overriding global Response object (v1.x) (#1411)
This commit is contained in:
parent
12ae856cee
commit
6e8f7e1a43
4 changed files with 122 additions and 19 deletions
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -9,7 +9,7 @@
|
|||
"version": "1.25.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
|
|
@ -665,9 +665,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
|
||||
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
|
|
@ -1332,6 +1332,7 @@
|
|||
"integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.48.1",
|
||||
"@typescript-eslint/types": "8.48.1",
|
||||
|
|
@ -1763,6 +1764,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -2305,6 +2307,7 @@
|
|||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -2623,6 +2626,7 @@
|
|||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
|
|
@ -4090,6 +4094,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4170,6 +4175,7 @@
|
|||
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.25.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
|
|
@ -4215,6 +4221,7 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -4409,6 +4416,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4422,6 +4430,7 @@
|
|||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -4574,6 +4583,7 @@
|
|||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
"client": "tsx scripts/cli.ts client"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
|
|
|
|||
|
|
@ -78,14 +78,19 @@ export class StreamableHTTPServerTransport 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 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -166,12 +171,17 @@ export class StreamableHTTPServerTransport 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
|
||||
|
|
|
|||
|
|
@ -2910,6 +2910,89 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('StreamableHTTPServerTransport 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 StreamableHTTPServerTransport({
|
||||
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 StreamableHTTPServerTransport({
|
||||
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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue