Fix(premature close) Redirect failing when response is chunked but empty (#1222)

* Fix redirect failing when response is chunked but empty. #1220 #1064

* Handle chunked responses where the final chunk and EOM code are in separate packets and where there is an additional data chunk in the same packet before the final chunk and EOM code.
This commit is contained in:
Travis D. Warlick, Jr 2021-08-12 12:37:22 -04:00 committed by GitHub
parent 136a5f1433
commit 51861e98a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 89 additions and 18 deletions

View File

@ -287,29 +287,44 @@ export default async function fetch(url, options_) {
}
function fixResponseChunkedTransferBadEnding(request, errorCallback) {
const LAST_CHUNK = Buffer.from('0\r\n');
let socket;
const LAST_CHUNK = Buffer.from('0\r\n\r\n');
request.on('socket', s => {
socket = s;
});
let isChunkedTransfer = false;
let properLastChunkReceived = false;
let previousChunk;
request.on('response', response => {
const {headers} = response;
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
let properLastChunkReceived = false;
isChunkedTransfer = headers['transfer-encoding'] === 'chunked' && !headers['content-length'];
});
socket.on('data', buf => {
properLastChunkReceived = Buffer.compare(buf.slice(-3), LAST_CHUNK) === 0;
});
request.on('socket', socket => {
const onSocketClose = () => {
if (isChunkedTransfer && !properLastChunkReceived) {
const error = new Error('Premature close');
error.code = 'ERR_STREAM_PREMATURE_CLOSE';
errorCallback(error);
}
};
socket.prependListener('close', () => {
if (!properLastChunkReceived) {
const error = new Error('Premature close');
error.code = 'ERR_STREAM_PREMATURE_CLOSE';
errorCallback(error);
}
});
}
socket.prependListener('close', onSocketClose);
request.on('abort', () => {
socket.removeListener('close', onSocketClose);
});
socket.on('data', buf => {
properLastChunkReceived = Buffer.compare(buf.slice(-5), LAST_CHUNK) === 0;
// Sometimes final 0-length chunk and end of message code are in separate packets
if (!properLastChunkReceived && previousChunk) {
properLastChunkReceived = (
Buffer.compare(previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 &&
Buffer.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0
);
}
previousChunk = buf;
});
});
}

View File

@ -684,6 +684,36 @@ describe('node-fetch', () => {
});
});
it('should follow redirect after empty chunked transfer-encoding', () => {
const url = `${base}redirect/chunked`;
return fetch(url).then(res => {
expect(res.status).to.equal(200);
expect(res.ok).to.be.true;
});
});
it('should handle chunked response with more than 1 chunk in the final packet', () => {
const url = `${base}chunked/multiple-ending`;
return fetch(url).then(res => {
expect(res.ok).to.be.true;
return res.text().then(result => {
expect(result).to.equal('foobar');
});
});
});
it('should handle chunked response with final chunk and EOM in separate packets', () => {
const url = `${base}chunked/split-ending`;
return fetch(url).then(res => {
expect(res.ok).to.be.true;
return res.text().then(result => {
expect(result).to.equal('foobar');
});
});
});
it('should handle DNS-error response', () => {
const url = 'http://domain.invalid';
return expect(fetch(url)).to.eventually.be.rejected

View File

@ -297,6 +297,14 @@ export default class TestServer {
res.socket.end('\r\n');
}
if (p === '/redirect/chunked') {
res.writeHead(301, {
Location: '/inspect',
'Transfer-Encoding': 'chunked'
});
setTimeout(() => res.end(), 10);
}
if (p === '/error/400') {
res.statusCode = 400;
res.setHeader('Content-Type', 'text/plain');
@ -344,6 +352,24 @@ export default class TestServer {
}, 400);
}
if (p === '/chunked/split-ending') {
res.socket.write('HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n');
res.socket.write('3\r\nfoo\r\n3\r\nbar\r\n');
setTimeout(() => {
res.socket.write('0\r\n');
}, 10);
setTimeout(() => {
res.socket.end('\r\n');
}, 20);
}
if (p === '/chunked/multiple-ending') {
res.socket.write('HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n');
res.socket.write('3\r\nfoo\r\n3\r\nbar\r\n0\r\n\r\n');
}
if (p === '/error/json') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');