diff --git a/src/http/core/request.spec.ts b/src/http/core/request.spec.ts index e751f05..dce3b1a 100644 --- a/src/http/core/request.spec.ts +++ b/src/http/core/request.spec.ts @@ -442,6 +442,24 @@ describe('UwsRequest', () => { ); }); + it('should optimize empty bodies by using the exact same memory reference', async () => { + const req1 = new UwsRequest(mockUwsReq, mockUwsRes); + const req2 = new UwsRequest(mockUwsReq, mockUwsRes); + + mockUwsReq.getMethod.mockReturnValue('GET'); + req1._initBodyParser(1024); + req2._initBodyParser(1024); + + onDataCallback(toArrayBuffer(Buffer.from('')), true); + + const res1 = await req1.json(); + const res2 = await req2.json(); + + // Referential equality proves zero new objects were allocated + expect(res1).toBe(res2); + expect(Object.isFrozen(res1)).toBe(true); + }); + it('should cache raw buffer', async () => { setHeaders(['content-length', '5']); diff --git a/src/http/core/request.ts b/src/http/core/request.ts index 70b40e3..b56053d 100644 --- a/src/http/core/request.ts +++ b/src/http/core/request.ts @@ -15,6 +15,13 @@ import { MultipartFormHandler } from '../body/multipart-handler'; */ const BUFFER_WATERMARK = 128 * 1024; // 128KB +/** + * This is a single shared constant which will be used inside the request class + * The object is created and frozen exactly once when the file is first loaded. Subsequent requests just point to this same memory address. + * Since no new objects are being created in that branch, the Garbage Collector has nothing to clean up, which improves the overall latency of the server. + */ +const EMPTY_FROZEN_OBJECT = Object.freeze({}); + /** * Headers that should NOT be duplicated per HTTP spec * @see https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2 @@ -1083,9 +1090,9 @@ export class UwsRequest extends Readable { // Handle empty body - return frozen empty object for GET/HEAD/DELETE, throw for all other methods if (text === '') { if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'DELETE') { - // Freeze the empty object to prevent accidental mutations + // Use the shared constant to freeze the empty object instead of creating a new one // This will throw TypeError in strict mode if mutation is attempted - this.cachedJson = Object.freeze({}) as T; + this.cachedJson = EMPTY_FROZEN_OBJECT as T; return this.cachedJson as T; } // Throw for POST/PUT/PATCH and other methods (OPTIONS, SEARCH, PROPFIND, etc.)