From 0353ea4015a711011ebbf3dd5dd364062c75aa70 Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Thu, 21 May 2026 21:13:01 +0000 Subject: [PATCH 1/2] fix(response-interceptor): reduce responseInterceptor buffer churn --------- Co-authored-by: Matt Fozard --- src/handlers/response-interceptor.ts | 21 ++++++++++--- test/unit/response-interceptor.spec.ts | 42 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/handlers/response-interceptor.ts b/src/handlers/response-interceptor.ts index 7721bb38..410fe874 100644 --- a/src/handlers/response-interceptor.ts +++ b/src/handlers/response-interceptor.ts @@ -47,15 +47,24 @@ export function responseInterceptor< ): Promise { debug('intercept proxy response'); const originalProxyRes = proxyRes; - let buffer = Buffer.from('', 'utf8'); + const chunks: Buffer[] = []; + let bufferLength = 0; // decompress proxy response const _proxyRes = decompress(proxyRes, proxyRes.headers['content-encoding']); - // concat data stream - _proxyRes.on('data', (chunk) => (buffer = Buffer.concat([buffer, chunk]))); + // collect data chunks and concatenate once on end to avoid repeated full-buffer copies + _proxyRes.on('data', (chunk) => { + const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + chunks.push(chunkBuffer); + bufferLength += chunkBuffer.length; // precalculate Buffer length for slightly better performance on Buffer.concat() + }); _proxyRes.on('end', async () => { + const buffer = Buffer.concat(chunks, bufferLength); + chunks.length = 0; // clear chunks array + bufferLength = 0; + // copy original headers copyHeaders(proxyRes, res); @@ -64,8 +73,8 @@ export function responseInterceptor< const interceptedBuffer = Buffer.from(await interceptor(buffer, originalProxyRes, req, res)); // set correct content-length (with double byte character support) - debug('set content-length: %s', Buffer.byteLength(interceptedBuffer, 'utf8')); - res.setHeader('content-length', Buffer.byteLength(interceptedBuffer, 'utf8')); + debug('set content-length: %s', Buffer.byteLength(interceptedBuffer)); + res.setHeader('content-length', Buffer.byteLength(interceptedBuffer)); debug('write intercepted response'); res.write(interceptedBuffer); @@ -73,6 +82,8 @@ export function responseInterceptor< }); _proxyRes.on('error', (error) => { + chunks.length = 0; // clear chunks array + bufferLength = 0; res.end(`Error fetching proxied request: ${error.message}`); }); }; diff --git a/test/unit/response-interceptor.spec.ts b/test/unit/response-interceptor.spec.ts index 0f24c7a1..d98bffd4 100644 --- a/test/unit/response-interceptor.spec.ts +++ b/test/unit/response-interceptor.spec.ts @@ -23,6 +23,48 @@ describe('responseInterceptor', () => { expect(res.end).toHaveBeenCalledWith(); }); + it('should combine proxy response chunks before calling interceptor', async () => { + const proxyRes = createMockRequest(); + const req = createMockRequest(); + const res = createMockResponse(); + + responseInterceptor(async (buffer) => { + expect(buffer).toEqual(Buffer.from('HPM')); + return buffer; + })(proxyRes, req, res); + + proxyRes.emit('data', Buffer.from('H')); + proxyRes.emit('data', Buffer.from('P')); + proxyRes.emit('data', Buffer.from('M')); + proxyRes.emit('end'); + await waitInterceptorHandler(); + + expect(res.setHeader).toHaveBeenCalledWith('content-length', 'HPM'.length); + expect(res.write).toHaveBeenCalledWith(Buffer.from('HPM')); + expect(res.end).toHaveBeenCalledWith(); + }); + + it('should combine string proxy response chunks before calling interceptor', async () => { + const proxyRes = createMockRequest(); + const req = createMockRequest(); + const res = createMockResponse(); + + responseInterceptor(async (buffer) => { + expect(buffer).toEqual(Buffer.from('HPM')); + return buffer; + })(proxyRes, req, res); + + proxyRes.emit('data', 'H'); + proxyRes.emit('data', 'P'); + proxyRes.emit('data', 'M'); + proxyRes.emit('end'); + await waitInterceptorHandler(); + + expect(res.setHeader).toHaveBeenCalledWith('content-length', 'HPM'.length); + expect(res.write).toHaveBeenCalledWith(Buffer.from('HPM')); + expect(res.end).toHaveBeenCalledWith(); + }); + it('should end with error when receive a proxy error event', async () => { const proxyRes = createMockRequest(); const req = createMockRequest(); From c5bd0b431921c681efbdf5bfce1f1509dd51fe14 Mon Sep 17 00:00:00 2001 From: Steven Chim <655241+chimurai@users.noreply.github.com> Date: Thu, 21 May 2026 21:14:32 +0000 Subject: [PATCH 2/2] docs(response-interceptor): reduce responseInterceptor buffer churn --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c8a420..784aaccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - chore(package.json): update to httpxy@0.5.3 - fix(ipv6): preserve credentials when normalizing bracketed IPv6 target string - fix(ipv6): unspecified IPv6 target hostname (::)" +- fix(response-interceptor): reduce responseInterceptor buffer churn ## [v4.0.0](https://github.com/chimurai/http-proxy-middleware/releases/tag/v4.0.0)