Skip to content

Commit dbcc481

Browse files
committed
Fix premature close with chunked Transfer-encoding in Node 12
1 parent f28d2b3 commit dbcc481

File tree

3 files changed

+75
-15
lines changed

3 files changed

+75
-15
lines changed

src/index.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export default async function fetch(url, options_) {
9595
finalize();
9696
});
9797

98+
fixResponseChunkedTransferBadEnding(request_, err => {
99+
response.body.destroy(err);
100+
});
101+
98102
request_.on('response', response_ => {
99103
request_.setTimeout(0);
100104
const headers = fromRawHeaders(response_.rawHeaders);
@@ -265,3 +269,31 @@ export default async function fetch(url, options_) {
265269
writeToStream(request_, request);
266270
});
267271
}
272+
273+
function fixResponseChunkedTransferBadEnding(request, errorCallback) {
274+
const LAST_CHUNK = Buffer.from('0\r\n');
275+
let socket;
276+
277+
request.on('socket', s => {
278+
socket = s;
279+
});
280+
281+
request.on('response', response => {
282+
const {headers} = response;
283+
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
284+
let properLastChunkReceived = false;
285+
286+
socket.on('data', buf => {
287+
properLastChunkReceived = Buffer.compare(buf.slice(-3), LAST_CHUNK) === 0;
288+
});
289+
290+
socket.prependListener('close', () => {
291+
if (!properLastChunkReceived) {
292+
const err = new Error('Premature close');
293+
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
294+
errorCallback(err);
295+
}
296+
});
297+
}
298+
});
299+
}

test/main.js

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -619,15 +619,43 @@ describe('node-fetch', () => {
619619
expect(res.status).to.equal(200);
620620
expect(res.ok).to.be.true;
621621

622-
const read = async (body) => {
623-
const chunks = [];
624-
for await (const chunk of body) {
625-
chunks.push(chunk);
626-
}
627-
return chunks;
628-
};
622+
return expect(new Promise((resolve, reject) => {
623+
res.body.on('error', reject);
624+
res.body.on('close', resolve);
625+
})).to.eventually.be.rejectedWith(Error, 'Premature close')
626+
.and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE');
627+
});
628+
});
629+
630+
// it('should handle network-error in chunked response 2', () => {
631+
// const url = `${base}error/premature/chunked`;
632+
// return fetch(url).then(res => {
633+
// expect(res.status).to.equal(200);
634+
// expect(res.ok).to.be.true;
635+
//
636+
// const read = async body => {
637+
// const chunks = [];
638+
// for await (const chunk of body) {
639+
// chunks.push(chunk);
640+
// }
641+
//
642+
// return chunks;
643+
// };
644+
//
645+
// return expect(read(res.body))
646+
// .to.eventually.be.rejectedWith(Error, 'Premature close')
647+
// .and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE');
648+
// });
649+
// });
650+
651+
it('should handle network-error in chunked response 2', () => {
652+
const url = `${base}error/premature/chunked`;
653+
return fetch(url).then(res => {
654+
expect(res.status).to.equal(200);
655+
expect(res.ok).to.be.true;
629656

630-
return expect(read(res.body)).to.eventually.be.rejectedWith(Error);
657+
return expect(res.text())
658+
.to.eventually.be.rejectedWith(Error, 'Premature close');
631659
});
632660
});
633661

test/utils/server.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,19 +325,19 @@ export default class TestServer {
325325

326326
if (p === '/error/premature/chunked') {
327327
res.writeHead(200, {
328-
'content-type': 'application/json',
329-
'transfer-encoding': 'chunked'
328+
'Content-Type': 'application/json',
329+
'Transfer-Encoding': 'chunked'
330330
});
331331

332-
res.write(`${JSON.stringify({ data: 'hi' })}\n`);
332+
res.write(`${JSON.stringify({data: 'hi'})}\n`);
333333

334334
setTimeout(() => {
335-
res.write(`${JSON.stringify({ data: 'bye' })}\n`);
336-
}, 100);
335+
res.write(`${JSON.stringify({data: 'bye'})}\n`);
336+
}, 200);
337337

338338
setTimeout(() => {
339-
res.destroy()
340-
}, 200);
339+
res.destroy();
340+
}, 400);
341341
}
342342

343343
if (p === '/error/json') {

0 commit comments

Comments
 (0)