diff --git a/lib/compression.js b/lib/compression.js index 3e4c692e4..6e28e6262 100755 --- a/lib/compression.js +++ b/lib/compression.js @@ -8,21 +8,23 @@ const Hoek = require('@hapi/hoek'); const internals = { - common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br'] + common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'br', 'gzip, deflate, br'] }; exports = module.exports = internals.Compression = class { decoders = { + br: (options) => Zlib.createBrotliDecompress(options), gzip: (options) => Zlib.createGunzip(options), deflate: (options) => Zlib.createInflate(options) }; - encodings = ['identity', 'gzip', 'deflate']; + encodings = ['identity', 'gzip', 'deflate', 'br']; encoders = { identity: null, + br: (options) => Zlib.createBrotliCompress(options), gzip: (options) => Zlib.createGzip(options), deflate: (options) => Zlib.createDeflate(options) }; @@ -45,7 +47,6 @@ exports = module.exports = internals.Compression = class { addEncoder(encoding, encoder) { - Hoek.assert(this.encoders[encoding] === undefined, `Cannot override existing encoder for ${encoding}`); Hoek.assert(typeof encoder === 'function', `Invalid encoder function for ${encoding}`); this.encoders[encoding] = encoder; this.encodings.unshift(encoding); @@ -54,7 +55,6 @@ exports = module.exports = internals.Compression = class { addDecoder(encoding, decoder) { - Hoek.assert(this.decoders[encoding] === undefined, `Cannot override existing decoder for ${encoding}`); Hoek.assert(typeof decoder === 'function', `Invalid decoder function for ${encoding}`); this.decoders[encoding] = decoder; } diff --git a/lib/types/server/encoders.d.ts b/lib/types/server/encoders.d.ts index c91fd7df3..68bb7ff3b 100644 --- a/lib/types/server/encoders.d.ts +++ b/lib/types/server/encoders.d.ts @@ -1,4 +1,4 @@ -import { createDeflate, createGunzip, createGzip, createInflate } from 'zlib'; +import { createBrotliCompress, createBrotliDecompress, createDeflate, createGunzip, createGzip, createInflate } from 'zlib'; /** * Available [content encoders](https://github.com/hapijs/hapi/blob/master/API.md#-serverencoderencoding-encoder). @@ -7,6 +7,7 @@ export interface ContentEncoders { deflate: typeof createDeflate; gzip: typeof createGzip; + br: typeof createBrotliCompress; } /** @@ -16,4 +17,5 @@ export interface ContentDecoders { deflate: typeof createInflate; gzip: typeof createGunzip; + br: typeof createBrotliDecompress; } diff --git a/test/payload.js b/test/payload.js index 0cfadedd6..9a758b99a 100755 --- a/test/payload.js +++ b/test/payload.js @@ -407,6 +407,30 @@ describe('Payload', () => { expect(res.result).to.equal(message); }); + it('handles br payload', async () => { + + const message = { 'msg': 'This message is going to be brotlied.' }; + const server = Hapi.server(); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + + const compressed = await new Promise((resolve) => Zlib.brotliCompress(JSON.stringify(message), (ignore, result) => resolve(result))); + + const request = { + method: 'POST', + url: '/', + headers: { + 'content-type': 'application/json', + 'content-encoding': 'br', + 'content-length': compressed.length + }, + payload: compressed + }; + + const res = await server.inject(request); + expect(res.result).to.exist(); + expect(res.result).to.equal(message); + }); + it('handles custom compression', async () => { const message = { 'msg': 'This message is going to be gzipped.' }; diff --git a/test/transmit.js b/test/transmit.js index 523bd24a7..433f6f013 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -677,6 +677,19 @@ describe('transmission', () => { expect(res3.headers['last-modified']).to.equal(res2.headers['last-modified']); }); + it('returns a brotlied file in the response when the request accepts br', async () => { + + const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } }); + await server.register(Inert); + server.route({ method: 'GET', path: '/file', handler: (request, h) => h.file(__dirname + '/../package.json') }); + + const res = await server.inject({ url: '/file', headers: { 'accept-encoding': 'br' } }); + expect(res.headers['content-type']).to.equal('application/json; charset=utf-8'); + expect(res.headers['content-encoding']).to.equal('br'); + expect(res.headers['content-length']).to.not.exist(); + expect(res.payload).to.exist(); + }); + it('returns a gzipped file in the response when the request accepts gzip', async () => { const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } }); @@ -729,6 +742,16 @@ describe('transmission', () => { expect(res.payload).to.exist(); }); + it('returns a brotlied stream response without a content-length header when accept-encoding is br', async () => { + + const server = Hapi.server({ compression: { minBytes: 1 } }); + server.route({ method: 'GET', path: '/stream', handler: () => new internals.TimerStream() }); + + const res = await server.inject({ url: '/stream', headers: { 'Content-Type': 'application/json', 'accept-encoding': 'br' } }); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-length']).to.not.exist(); + }); + it('returns a gzipped stream response without a content-length header when accept-encoding is gzip', async () => { const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -749,6 +772,37 @@ describe('transmission', () => { expect(res.headers['content-length']).to.not.exist(); }); + it('returns a br response on a post request when accept-encoding: br is requested', async () => { + + const data = '{"test":"true"}'; + + const server = Hapi.server({ compression: { minBytes: 1 } }); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const brotlied = await internals.compress('brotliCompress', Buffer.from(data)); + + const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'br' }, payload: data }); + expect(payload.toString()).to.equal(brotlied.toString()); + await server.stop(); + }); + + it('returns a br response on a get request when accept-encoding: br is requested', async () => { + + const data = '{"test":"true"}'; + + const server = Hapi.server({ compression: { minBytes: 1 } }); + server.route({ method: 'GET', path: '/', handler: () => data }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const brotlied = await internals.compress('brotliCompress', Buffer.from(data)); + const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'br' } }); + expect(payload.toString()).to.equal(brotlied.toString()); + await server.stop(); + }); + it('returns a gzip response on a post request when accept-encoding: gzip is requested', async () => { const data = '{"test":"true"}'; @@ -835,7 +889,7 @@ describe('transmission', () => { await server.stop(); }); - it('returns a gzip response on a post request when accept-encoding: gzip;q=1, deflate;q=0.5 is requested', async () => { + it('returns a gzip response on a post request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=0.7 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -844,12 +898,12 @@ describe('transmission', () => { const uri = 'http://localhost:' + server.info.port; const zipped = await internals.compress('gzip', Buffer.from(data)); - const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5' }, payload: data }); + const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5, br;q=0.7' }, payload: data }); expect(payload.toString()).to.equal(zipped.toString()); await server.stop(); }); - it('returns a gzip response on a get request when accept-encoding: gzip;q=1, deflate;q=0.5 is requested', async () => { + it('returns a gzip response on a get request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=0.7 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -858,12 +912,12 @@ describe('transmission', () => { const uri = 'http://localhost:' + server.info.port; const zipped = await internals.compress('gzip', Buffer.from(data)); - const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5' } }); + const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5, br;q=0.7' } }); expect(payload.toString()).to.equal(zipped.toString()); await server.stop(); }); - it('returns a deflate response on a post request when accept-encoding: deflate;q=1, gzip;q=0.5 is requested', async () => { + it('returns a deflate response on a post request when accept-encoding: deflate;q=1, gzip;q=0.5, br;q=0.7 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -872,12 +926,12 @@ describe('transmission', () => { const uri = 'http://localhost:' + server.info.port; const deflated = await internals.compress('deflate', Buffer.from(data)); - const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'deflate;q=1, gzip;q=0.5' }, payload: data }); + const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'deflate;q=1, gzip;q=0.5, br;q=0.7' }, payload: data }); expect(payload.toString()).to.equal(deflated.toString()); await server.stop(); }); - it('returns a deflate response on a get request when accept-encoding: deflate;q=1, gzip;q=0.5 is requested', async () => { + it('returns a deflate response on a get request when accept-encoding: deflate;q=1, gzip;q=0.5, br;q=0.7 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -886,7 +940,7 @@ describe('transmission', () => { const uri = 'http://localhost:' + server.info.port; const deflated = await internals.compress('deflate', Buffer.from(data)); - const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'deflate;q=1, gzip;q=0.5' } }); + const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'deflate;q=1, gzip;q=0.5, br;q=0.7' } }); expect(payload.toString()).to.equal(deflated.toString()); await server.stop(); });