Skip to content

Commit

Permalink
Backport node-fetch redirect bugfix
Browse files Browse the repository at this point in the history
node-fetch PR that this work is based upon:
node-fetch/node-fetch#1222
  • Loading branch information
BenMcH committed Jul 1, 2022
1 parent f5a5e15 commit 89df20c
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 18 deletions.
54 changes: 36 additions & 18 deletions packages/fetch/src/fetch.js
Expand Up @@ -324,29 +324,47 @@ async function fetch(url, options_ = {}) {
* @param {(error:Error) => void} errorCallback
*/
function fixResponseChunkedTransferBadEnding(request, errorCallback) {
/** @type {import('net').Socket} */
let socket;
const LAST_CHUNK = Buffer.from('0\r\n\r\n');

request.on('socket', s => {
socket = s;
});
let isChunkedTransfer = false;
let properLastChunkReceived = false;
/** @type {Buffer | undefined} */
let previousChunk;

request.on('response', response => {

const {headers} = response;
isChunkedTransfer = headers['transfer-encoding'] === 'chunked' && !headers['content-length'];
});

if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
socket.prependListener('close', hadError => {
// if a data listener is still present we didn't end cleanly
const hasDataListener = socket.listenerCount('data') > 0;
if (hasDataListener && !hadError) {
const err = Object.assign(new Error('Premature close'), {
code: 'ERR_STREAM_PREMATURE_CLOSE'
})
errorCallback(err);
}
});
}
request.on('socket', socket => {
const onSocketClose = () => {
if (isChunkedTransfer && !properLastChunkReceived) {
const error = Object.assign(new Error('Premature close'), {
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;
});
});
}

Expand Down
30 changes: 30 additions & 0 deletions packages/fetch/test/main.js
Expand Up @@ -665,6 +665,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.skip('should handle DNS-error response', () => {
const url = 'http://domain.invalid';
return expect(fetch(url)).to.eventually.be.rejected
Expand Down
26 changes: 26 additions & 0 deletions packages/fetch/test/utils/server.js
Expand Up @@ -310,6 +310,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');
Expand Down Expand Up @@ -357,6 +365,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');
Expand Down

0 comments on commit 89df20c

Please sign in to comment.