diff --git a/src/index.js b/src/index.js index 9e1151bc7..4799917b4 100644 --- a/src/index.js +++ b/src/index.js @@ -287,35 +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 prevChunk; 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); + } + }; - const onSocketClose = () => { - if (!properLastChunkReceived) { - const error = new Error('Premature close'); - error.code = 'ERR_STREAM_PREMATURE_CLOSE'; - errorCallback(error); - } - }; + socket.prependListener('close', onSocketClose); - socket.prependListener('close', onSocketClose); + request.on('abort', () => { + socket.removeListener('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 && prevChunk) { + properLastChunkReceived = ( + Buffer.compare(prevChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 && + Buffer.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0 + ); + } + + prevChunk = buf; + }); }); } diff --git a/test/main.js b/test/main.js index a777cf583..1e1f368c3 100644 --- a/test/main.js +++ b/test/main.js @@ -692,6 +692,28 @@ describe('node-fetch', () => { }); }); + 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 diff --git a/test/utils/server.js b/test/utils/server.js index 32aa070fe..9bfe1c5af 100644 --- a/test/utils/server.js +++ b/test/utils/server.js @@ -352,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');