Skip to content

Commit

Permalink
Fix premature close with chunked Transfer-encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
tekwiz committed Jan 9, 2021
1 parent f28d2b3 commit 47b00e1
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 9 deletions.
32 changes: 32 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export default async function fetch(url, options_) {
finalize();
});

fixResponseChunkedTransferBadEnding(request_, err => {
response.body.destroy(err);
});

request_.on('response', response_ => {
request_.setTimeout(0);
const headers = fromRawHeaders(response_.rawHeaders);
Expand Down Expand Up @@ -265,3 +269,31 @@ export default async function fetch(url, options_) {
writeToStream(request_, request);
});
}

function fixResponseChunkedTransferBadEnding(request, errorCallback) {
const LAST_CHUNK = Buffer.from('0\r\n');
let socket;

request.on('socket', s => {
socket = s;
});

request.on('response', response => {
const {headers} = response;
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
let properLastChunkReceived = false;

socket.on('data', buf => {
properLastChunkReceived = Buffer.compare(buf.slice(-3), LAST_CHUNK) === 0;
});

socket.prependListener('close', () => {
if (!properLastChunkReceived) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
errorCallback(err);
}
});
}
});
}
32 changes: 30 additions & 2 deletions test/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,15 +619,43 @@ describe('node-fetch', () => {
expect(res.status).to.equal(200);
expect(res.ok).to.be.true;

const read = async (body) => {
return expect(new Promise((resolve, reject) => {
res.body.on('error', reject);
res.body.on('close', resolve);
})).to.eventually.be.rejectedWith(Error, 'Premature close')
.and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE');
});
});

it('should handle network-error in chunked response async iterator', () => {
const url = `${base}error/premature/chunked`;
return fetch(url).then(res => {
expect(res.status).to.equal(200);
expect(res.ok).to.be.true;

const read = async body => {
const chunks = [];
for await (const chunk of body) {
chunks.push(chunk);
}

return chunks;
};

return expect(read(res.body)).to.eventually.be.rejectedWith(Error);
return expect(read(res.body))
.to.eventually.be.rejectedWith(Error, 'Premature close')
.and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE');
});
});

it('should handle network-error in chunked response in consumeBody', () => {
const url = `${base}error/premature/chunked`;
return fetch(url).then(res => {
expect(res.status).to.equal(200);
expect(res.ok).to.be.true;

return expect(res.text())
.to.eventually.be.rejectedWith(Error, 'Premature close');
});
});

Expand Down
14 changes: 7 additions & 7 deletions test/utils/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,19 +325,19 @@ export default class TestServer {

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

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

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

setTimeout(() => {
res.destroy()
}, 200);
res.destroy();
}, 400);
}

if (p === '/error/json') {
Expand Down

0 comments on commit 47b00e1

Please sign in to comment.