Skip to content

Commit da3bad3

Browse files
chimuraiziboilihua
andauthored
fix(responseInterceptor): prevent trailer/content-length conflict
Causing this error to be thrown: ```shell node:_http_outgoing:522 throw new ERR_HTTP_TRAILER_INVALID(); ^ Error [ERR_HTTP_TRAILER_INVALID]: Trailers are invalid with this transfer encoding ``` --------- Co-authored-by: zibo <ziboilihua@gmail.com>
1 parent 5a78782 commit da3bad3

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

src/handlers/response-interceptor.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export function responseInterceptor<
7474

7575
// set correct content-length (with double byte character support)
7676
debug('set content-length: %s', Buffer.byteLength(interceptedBuffer));
77+
78+
// Buffered responses cannot preserve trailer framing.
79+
// Remove trailer declaration (and transfer-encoding just in case) before setting content-length.
80+
res.removeHeader('trailer');
81+
res.removeHeader('transfer-encoding');
7782
res.setHeader('content-length', Buffer.byteLength(interceptedBuffer));
7883

7984
debug('write intercepted response');
@@ -143,8 +148,10 @@ function copyHeaders<TRes extends http.ServerResponse = http.ServerResponse>(
143148
if (response.setHeader) {
144149
let keys = Object.keys(originalResponse.headers);
145150

146-
// ignore chunked, brotli, gzip, deflate headers
147-
keys = keys.filter((key) => !['content-encoding', 'transfer-encoding'].includes(key));
151+
// ignore encoding/framing headers that are incompatible with buffered interception
152+
keys = keys.filter(
153+
(key) => !['content-encoding', 'transfer-encoding', 'trailer'].includes(key),
154+
);
148155

149156
keys.forEach((key) => {
150157
let value = originalResponse.headers[key];

test/e2e/response-interceptor.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,49 @@ describe('responseInterceptor()', () => {
157157
expect(response.body.deflated).toBe(true);
158158
});
159159
});
160+
161+
describe('trailer response header handling', () => {
162+
beforeEach(async () => {
163+
await targetServer
164+
.forGet('/response-headers')
165+
.withExactQuery('?Trailer=X-Stream-Error&Host=localhost')
166+
.thenReply(200, '', {
167+
'transfer-encoding': 'chunked',
168+
trailer: 'X-Stream-Error',
169+
host: 'localhost',
170+
'set-cookie': 'cookie=monster; Path=/',
171+
'content-type': 'application/json; charset=utf-8',
172+
'cache-control': 'private, max-age=60',
173+
});
174+
175+
agent = request(
176+
createApp(
177+
createProxyMiddleware({
178+
target: targetServer.url,
179+
changeOrigin: true,
180+
selfHandleResponse: true,
181+
on: {
182+
proxyRes: responseInterceptor(async () => {
183+
return JSON.stringify({ ok: true, note: 'rewritten' });
184+
}),
185+
},
186+
}),
187+
),
188+
);
189+
});
190+
191+
it('should rewrite body without trailer/content-length conflict and preserve end-to-end headers', async () => {
192+
const response = await agent
193+
.get('/response-headers?Trailer=X-Stream-Error&Host=localhost')
194+
.expect(200);
195+
196+
expect(response.body).toEqual({ ok: true, note: 'rewritten' });
197+
expect(response.header['trailer']).toBeUndefined();
198+
expect(response.header['transfer-encoding']).toBeUndefined();
199+
expect(response.header['content-length']).toBeDefined();
200+
expect(response.header['content-type']).toContain('application/json');
201+
expect(response.header['set-cookie']).toBeDefined();
202+
expect(response.header['cache-control']).toBe('private, max-age=60');
203+
});
204+
});
160205
});

0 commit comments

Comments
 (0)