diff --git a/lib/file.js b/lib/file.js index acf6465..2e66c80 100755 --- a/lib/file.js +++ b/lib/file.js @@ -148,22 +148,30 @@ internals.addContentRange = function (response, callback) { if (!request.headers['if-range'] || request.headers['if-range'] === response.headers.etag) { // Ignoring last-modified date (weak) - // Parse header + // Check that response is not encoded once transmitted - const ranges = Ammo.header(request.headers.range, length); - if (!ranges) { - const error = Boom.rangeNotSatisfiable(); - error.output.headers['content-range'] = 'bytes */' + length; - return callback(error); - } + const mime = request.server.mime.type(response.headers['content-type'] || 'application/octet-stream'); + const encoding = (request.connection.settings.compression && mime.compressible && !response.headers['content-encoding'] ? request.info.acceptEncoding : null); + + if (encoding === 'identity' || !encoding) { + + // Parse header + + const ranges = Ammo.header(request.headers.range, length); + if (!ranges) { + const error = Boom.rangeNotSatisfiable(); + error.output.headers['content-range'] = 'bytes */' + length; + return callback(error); + } - // Prepare transform + // Prepare transform - if (ranges.length === 1) { // Ignore requests for multiple ranges - range = ranges[0]; - response.code(206); - response.bytes(range.to - range.from + 1); - response.header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length); + if (ranges.length === 1) { // Ignore requests for multiple ranges + range = ranges[0]; + response.code(206); + response.bytes(range.to - range.from + 1); + response.header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length); + } } } } diff --git a/test/file.js b/test/file.js index 842dc2e..89c0027 100755 --- a/test/file.js +++ b/test/file.js @@ -1401,6 +1401,83 @@ describe('file', () => { }); }); + it('reads partial file content for a non-compressible file', (done) => { + + const server = provisionServer(); + server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/image.png'), etagMethod: false } } }); + + // Catch createReadStream options + + let createOptions; + const orig = Fs.createReadStream; + Fs.createReadStream = function (path, options) { + + Fs.createReadStream = orig; + createOptions = options; + + return Fs.createReadStream(path, options); + }; + + server.inject({ url: '/file', headers: { 'range': 'bytes=1-4', 'accept-encoding': 'gzip' } }, (res) => { + + expect(res.statusCode).to.equal(206); + expect(res.headers['content-length']).to.equal(4); + expect(res.headers['content-range']).to.equal('bytes 1-4/42010'); + expect(res.headers['accept-ranges']).to.equal('bytes'); + expect(res.rawPayload).to.deep.equal(new Buffer('PNG\r', 'ascii')); + expect(createOptions).to.include({ start: 1, end: 4 }); + done(); + }); + }); + + it('returns 200 when content-length is missing', (done) => { + + const server = provisionServer(); + server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/image.png') } } }); + + server.ext('onPreResponse', (request, reply) => { + + delete request.response.headers['content-length']; + return reply.continue(); + }); + + server.inject({ url: '/file', headers: { 'range': 'bytes=1-5' } }, (res) => { + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-length']).to.not.exist(); + done(); + }); + }); + + it('returns 200 for dynamically compressed responses', (done) => { + + const server = provisionServer(); + server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt'), lookupCompressed: false } } }); + server.inject({ url: '/file', headers: { 'range': 'bytes=1-3', 'accept-encoding': 'gzip' } }, (res) => { + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-encoding']).to.equal('gzip'); + expect(res.headers['content-length']).to.not.exist(); + expect(res.headers['content-range']).to.not.exist(); + expect(res.headers['accept-ranges']).to.equal('bytes'); + done(); + }); + }); + + it('returns a subset of a file when compression is disabled', (done) => { + + const server = provisionServer({ compression: false }); + server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt'), lookupCompressed: false } } }); + server.inject({ url: '/file', headers: { 'range': 'bytes=1-3', 'accept-encoding': 'gzip' } }, (res) => { + + expect(res.statusCode).to.equal(206); + expect(res.headers['content-encoding']).to.not.exist(); + expect(res.headers['content-length']).to.equal(3); + expect(res.headers['content-range']).to.equal('bytes 1-3/4'); + done(); + }); + }); + it('returns a subset of a file using precompressed file', (done) => { const server = provisionServer(); @@ -1416,6 +1493,42 @@ describe('file', () => { done(); }); }); + + it('returns a subset for dynamically compressed responses with "identity" encoding', (done) => { + + const server = provisionServer(); + server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt'), lookupCompressed: false } } }); + server.inject({ url: '/file', headers: { 'range': 'bytes=1-3', 'accept-encoding': 'identity' } }, (res) => { + + expect(res.statusCode).to.equal(206); + expect(res.headers['content-encoding']).to.not.exist(); + expect(res.headers['content-length']).to.equal(3); + expect(res.headers['content-range']).to.equal('bytes 1-3/4'); + done(); + }); + }); + + it('returns a subset when content-type is missing', (done) => { + + const server = provisionServer(); + server.route({ method: 'GET', path: '/file', handler: { file: { path: Path.join(__dirname, 'file/note.txt') } } }); + + server.ext('onPreResponse', (request, reply) => { + + delete request.response.headers['content-type']; + return reply.continue(); + }); + + server.inject({ url: '/file', headers: { 'range': 'bytes=1-5' } }, (res) => { + + expect(res.statusCode).to.equal(206); + expect(res.headers['content-encoding']).to.not.exist(); + expect(res.headers['content-length']).to.equal(3); + expect(res.headers['content-range']).to.equal('bytes 1-3/4'); + expect(res.headers['content-type']).to.not.exist(); + done(); + }); + }); }); it('has not leaked file descriptors', { skip: process.platform === 'win32' }, (done) => {