diff --git a/API.md b/API.md index 7b9316f06..fdcc44881 100755 --- a/API.md +++ b/API.md @@ -98,10 +98,35 @@ assigned one or more (array): #### `server.options.compression` -Default value: `{ minBytes: 1024 }`. +Default value: `{ decompress: true, encodings: { gzip: true, deflate: true, br: false, zstd: false }, minBytes: 1024 }`. Defines server handling of content encoding requests. If `false`, response content encoding is -disabled and no compression is performed by the server. +disabled, and no compression is performed by the server. + +#### `server.options.compression.decompress` + +Default value: `true`. + +Controls whether the server automatically decompresses incoming content encoding requests. +If `false`, no decompression automatically performed by the server. + +#### `server.options.compression.encodings` + +Default value: `{ gzip: true, deflate: true, br: false, zstd: false }`. + +Configures the built-in support of compression algorithms aka encodings, represented by the object of kv pairs. + +Available values for each kv pair: + +- `true` - enables the encoding with default options. +- `false` - disables the encoding. + +Note that default encoder and decoder options can be configured at the server default [route configuration](#server.options.routes) + +Zstd compression is experimental (see [node Zstd documentation](https://nodejs.org/api/zlib.html#zlibcreatezstdcompressoptions)). + +Disabling an encoding allows custom compression algorithm to be applied by +[`server.encoder()`](#server.encoder()) and [`server.decoder()`](#server.decoder()). ##### `server.options.compression.minBytes` @@ -110,6 +135,13 @@ Default value: '1024'. Sets the minimum response payload size in bytes that is required for content encoding compression. If the payload size is under the limit, no compression is performed. +##### `server.options.compression.priority` + +Default value: `null`. + +Sets the priority for content encoding compression algorithms in descending order, +e.g.: `['zstd', 'br', 'gzip', 'deflate']`. + #### `server.options.debug` Default value: `{ request: ['implementation'] }`. @@ -1290,8 +1322,7 @@ are called, where: ### `server.decoder(encoding, decoder)` -Registers a custom content decoding compressor to extend the built-in support for `'gzip'` and -'`deflate`' where: +Registers a custom content decoding compressor to extend the built-in support where: - `encoding` - the decoder name string. @@ -1487,8 +1518,7 @@ The `dependencies` configuration accepts one of: ### `server.encoder(encoding, encoder)` -Registers a custom content encoding compressor to extend the built-in support for `'gzip'` and -'`deflate`' where: +Registers a custom content encoding compressor to extend the built-in support where: - `encoding` - the encoder name string. diff --git a/lib/compression.js b/lib/compression.js index 3e4c692e4..4a8889816 100755 --- a/lib/compression.js +++ b/lib/compression.js @@ -5,33 +5,87 @@ const Zlib = require('zlib'); const Accept = require('@hapi/accept'); const Bounce = require('@hapi/bounce'); const Hoek = require('@hapi/hoek'); +const Boom = require('@hapi/boom'); +const defaultBrotliOptions = { + params: { + [Zlib.constants.BROTLI_PARAM_QUALITY]: 4 + } +}; + +const defaultZstdOptions = { + params: { + [Zlib.constants.ZSTD_c_compressionLevel]: 6 + } +}; const internals = { - common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br'] + common: [ + 'gzip, deflate, br, zstd', + 'gzip, deflate, br', + 'zstd', + 'br', + 'gzip, deflate', + 'deflate, gzip', + 'gzip', + 'deflate' + ], + provision: new Map([ + ['zstd', [ + (options = {}) => Zlib.createZstdCompress(Hoek.applyToDefaults(defaultZstdOptions, options)), + (options) => Zlib.createZstdDecompress(options) + ]], + ['br', [ + (options = {}) => Zlib.createBrotliCompress(Hoek.applyToDefaults(defaultBrotliOptions, options)), + (options) => Zlib.createBrotliDecompress(options) + ]], + ['deflate', [ + (options) => Zlib.createDeflate(options), + (options) => Zlib.createInflate(options) + ]], + ['gzip', [ + (options) => Zlib.createGzip(options), + (options) => Zlib.createGunzip(options) + ]] + ]) }; exports = module.exports = internals.Compression = class { - decoders = { - gzip: (options) => Zlib.createGunzip(options), - deflate: (options) => Zlib.createInflate(options) - }; + decoders = {}; - encodings = ['identity', 'gzip', 'deflate']; + encodings = ['identity']; encoders = { - identity: null, - gzip: (options) => Zlib.createGzip(options), - deflate: (options) => Zlib.createDeflate(options) + identity: null }; #common = null; + #options = null; - constructor() { + constructor(options) { - this._updateCommons(); + this.#options = options; + if (!this.#options) { + return this._updateCommons(); + } + + for (const [encoding, [encoder, decoder]] of internals.provision.entries()) { + const conditions = this.#options?.encodings?.[encoding]; + if (conditions) { + this.addEncoder(encoding, encoder); + if (this.#options.decompress !== false) { + this.addDecoder(encoding, decoder); + } + else { + this.addDecoder(encoding, () => { + + throw Boom.unsupportedMediaType(); + }); + } + } + } } _updateCommons() { @@ -89,13 +143,13 @@ exports = module.exports = internals.Compression = class { return null; } - const request = response.request; - if (!request._core.settings.compression || - length !== null && length < request._core.settings.compression.minBytes) { + if (!this.#options || + length !== null && length < this.#options.minBytes) { return null; } + const request = response.request; const mime = request._core.mime.type(response.headers['content-type'] || 'application/octet-stream'); if (!mime.compressible) { return null; @@ -116,4 +170,10 @@ exports = module.exports = internals.Compression = class { Hoek.assert(encoder !== undefined, `Unknown encoding ${encoding}`); return encoder(request.route.settings.compression[encoding]); } + + setPriority(priority) { + + this.encodings = [...new Set([...priority, ...this.encodings])]; + this._updateCommons(); + } }; diff --git a/lib/config.js b/lib/config.js index 2b668f97d..1153c135a 100755 --- a/lib/config.js +++ b/lib/config.js @@ -241,7 +241,15 @@ internals.server = Validate.object({ autoListen: Validate.boolean(), cache: Validate.allow(null), // Validated elsewhere compression: Validate.object({ - minBytes: Validate.number().min(1).integer().default(1024) + decompress: Validate.boolean().default(true), + encodings: Validate.object({ + gzip: Validate.boolean().default(true), + deflate: Validate.boolean().default(true), + br: Validate.boolean().default(false), + zstd: Validate.boolean().default(false) + }).default(), + minBytes: Validate.number().min(1).integer().default(1024), + priority: Validate.array().items(Validate.string().valid('gzip', 'deflate', 'br', 'zstd')).default(null) }) .allow(false) .default(), diff --git a/lib/core.js b/lib/core.js index 202f6dbe0..42691c00f 100755 --- a/lib/core.js +++ b/lib/core.js @@ -54,7 +54,7 @@ exports = module.exports = internals.Core = class { app = {}; auth = new Auth(this); caches = new Map(); // Cache clients - compression = new Compression(); + compression = null; controlled = null; // Other servers linked to the phases of this server dependencies = []; // Plugin dependencies events = new Podium.Podium(internals.events); @@ -119,6 +119,7 @@ exports = module.exports = internals.Core = class { this.settings = settings; this.type = type; + this.compression = new Compression(this.settings.compression); this.heavy = new Heavy(this.settings.load); this.mime = new Mimos(this.settings.mime); this.router = new Call.Router(this.settings.router); @@ -127,6 +128,10 @@ exports = module.exports = internals.Core = class { this._debug(); this._initializeCache(); + if (this.settings.compression.priority) { + this.compression.setPriority(this.settings.compression.priority); + } + if (this.settings.routes.validate.validator) { this.validator = Validation.validator(this.settings.routes.validate.validator); } diff --git a/lib/types/server/encoders.d.ts b/lib/types/server/encoders.d.ts index c91fd7df3..96d89a259 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, createZstdCompress, createZstdDecompress } from 'zlib'; /** * Available [content encoders](https://github.com/hapijs/hapi/blob/master/API.md#-serverencoderencoding-encoder). @@ -7,6 +7,8 @@ export interface ContentEncoders { deflate: typeof createDeflate; gzip: typeof createGzip; + br: typeof createBrotliCompress; + zstd: typeof createZstdCompress; } /** @@ -16,4 +18,6 @@ export interface ContentDecoders { deflate: typeof createInflate; gzip: typeof createGunzip; + br: typeof createBrotliDecompress; + zstd: typeof createZstdDecompress; } diff --git a/lib/types/server/options.d.ts b/lib/types/server/options.d.ts index 5b7c9a7e3..8e294341b 100644 --- a/lib/types/server/options.d.ts +++ b/lib/types/server/options.d.ts @@ -9,7 +9,15 @@ import { CacheProvider, ServerOptionsCache } from './cache'; import { SameSitePolicy, ServerStateCookieOptions } from './state'; export interface ServerOptionsCompression { + decompress?: boolean; + encodings?: { + gzip?: boolean; + deflate?: boolean; + br?: boolean; + zstd?: boolean; + }; minBytes: number; + priority?: string[]; } /** diff --git a/lib/types/server/server.d.ts b/lib/types/server/server.d.ts index 4d64710e1..e32b8faf0 100644 --- a/lib/types/server/server.d.ts +++ b/lib/types/server/server.d.ts @@ -332,7 +332,7 @@ export class Server { cache: ServerCache; /** - * Registers a custom content decoding compressor to extend the built-in support for 'gzip' and 'deflate' where: + * Registers a custom content decoding compressor to extend the built-in support where: * @param encoding - the decoder name string. * @param decoder - a function using the signature function(options) where options are the encoding specific options configured in the route payload.compression configuration option, and the * return value is an object compatible with the output of node's zlib.createGunzip(). @@ -392,7 +392,7 @@ export class Server { dependency(dependencies: Dependencies, after?: ((server: Server) => Promise) | undefined): void; /** - * Registers a custom content encoding compressor to extend the built-in support for 'gzip' and 'deflate' where: + * Registers a custom content encoding compressor to extend the built-in support where: * @param encoding - the encoder name string. * @param encoder - a function using the signature function(options) where options are the encoding specific options configured in the route compression option, and the return value is an object * compatible with the output of node's zlib.createGzip(). diff --git a/package.json b/package.json index ec7ba6c99..43e2f991d 100755 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@hapi/somever": "^4.1.1", "@hapi/statehood": "^8.2.0", "@hapi/subtext": "^8.1.1", - "@hapi/teamwork": "^6.0.0", + "@hapi/teamwork": "^6.0.1", "@hapi/topo": "^6.0.2", "@hapi/validate": "^2.0.1" }, @@ -47,14 +47,14 @@ "@hapi/code": "^9.0.3", "@hapi/eslint-plugin": "^6.0.0", "@hapi/inert": "^7.1.0", - "@hapi/joi-legacy-test": "npm:@hapi/joi@^15.0.0", + "@hapi/joi-legacy-test": "npm:@hapi/joi@^15.1.1", "@hapi/lab": "^25.3.2", "@hapi/vision": "^7.0.3", "@hapi/wreck": "^18.1.0", - "@types/node": "^18.19.122", + "@types/node": "^22.18.3", "handlebars": "^4.7.8", "joi": "^17.13.3", - "legacy-readable-stream": "npm:readable-stream@^1.0.34", + "legacy-readable-stream": "npm:readable-stream@^1.1.14", "typescript": "^4.9.5" }, "scripts": { diff --git a/test/common.js b/test/common.js index eae48a36b..265de6992 100644 --- a/test/common.js +++ b/test/common.js @@ -3,6 +3,7 @@ const ChildProcess = require('child_process'); const Http = require('http'); const Net = require('net'); +const Zlib = require('zlib'); const internals = {}; @@ -30,3 +31,5 @@ internals.hasIPv6 = () => { exports.hasLsof = internals.hasLsof(); exports.hasIPv6 = internals.hasIPv6(); + +exports.hasZstd = !!Zlib.constants.ZSTD_CLEVEL_DEFAULT; diff --git a/test/payload.js b/test/payload.js index 93b045cdf..b1e53864f 100755 --- a/test/payload.js +++ b/test/payload.js @@ -14,6 +14,8 @@ const Hoek = require('@hapi/hoek'); const Lab = require('@hapi/lab'); const Wreck = require('@hapi/wreck'); +const Common = require('./common'); + const internals = {}; @@ -477,6 +479,30 @@ describe('Payload', () => { expect(peeked).to.be.true(); }); + it('rejects compressed payload when decompression is disabled', async () => { + + const message = { 'msg': 'This message is going to be gzipped.' }; + const server = Hapi.server({ compression: { decompress: false } }); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + + const compressed = await new Promise((resolve) => Zlib.gzip(JSON.stringify(message), (ignore, result) => resolve(result))); + + const request = { + method: 'POST', + url: '/', + headers: { + 'content-type': 'application/json', + 'content-encoding': 'gzip', + 'content-length': compressed.length + }, + payload: compressed + }; + + const res = await server.inject(request); + expect(res.result).to.exist(); + expect(res.statusCode).to.equal(415); + }); + it('handles gzipped payload', async () => { const message = { 'msg': 'This message is going to be gzipped.' }; @@ -525,6 +551,54 @@ 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({ compression: { encodings: { br: true } } }); + 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 zstd payload', { skip: !Common.hasZstd }, async () => { + + const message = { 'msg': 'This message is going to be zstded.' }; + const server = Hapi.server({ compression: { encodings: { zstd: true } } }); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + + const compressed = await new Promise((resolve) => Zlib.zstdCompress(JSON.stringify(message), (ignore, result) => resolve(result))); + + const request = { + method: 'POST', + url: '/', + headers: { + 'content-type': 'application/json', + 'content-encoding': 'zstd', + '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 b6f1fd7cd..d22242dda 100755 --- a/test/transmit.js +++ b/test/transmit.js @@ -677,6 +677,32 @@ describe('transmission', () => { expect(res3.headers['last-modified']).to.equal(res2.headers['last-modified']); }); + it('returns a zstded file in the response when the request accepts zstd', { skip: !Common.hasZstd }, async () => { + + const server = Hapi.server({ compression: { encodings: { zstd: true }, 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': 'zstd' } }); + expect(res.headers['content-type']).to.equal('application/json; charset=utf-8'); + expect(res.headers['content-encoding']).to.equal('zstd'); + expect(res.headers['content-length']).to.not.exist(); + expect(res.payload).to.exist(); + }); + + it('returns a brotlied file in the response when the request accepts br', async () => { + + const server = Hapi.server({ compression: { encodings: { br: true }, 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 +755,39 @@ describe('transmission', () => { expect(res.payload).to.exist(); }); + it('returns a deflated file in the response when the request accepts gzip, deflate but priority set to deflate', async () => { + + const server = Hapi.server({ compression: { minBytes: 1, priority: ['deflate'] }, 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': 'gzip, deflate' } }); + expect(res.headers['content-type']).to.equal('application/json; charset=utf-8'); + expect(res.headers['content-encoding']).to.equal('deflate'); + expect(res.headers['content-length']).to.not.exist(); + expect(res.payload).to.exist(); + }); + + it('returns a zstded stream response without a content-length header when accept-encoding is zstd', async () => { + + const server = Hapi.server({ compression: { encodings: { zstd: true }, 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': 'zstd' } }); + expect(res.statusCode).to.equal(200); + expect(res.headers['content-length']).to.not.exist(); + }); + + it('returns a brotlied stream response without a content-length header when accept-encoding is br', async () => { + + const server = Hapi.server({ compression: { encodings: { br: true }, 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 +808,66 @@ describe('transmission', () => { expect(res.headers['content-length']).to.not.exist(); }); + it('returns a zstd response on a post request when accept-encoding: zstd is requested', { skip: !Common.hasZstd }, async () => { + + const data = '{"test":"true"}'; + + const server = Hapi.server({ compression: { encodings: { zstd: true }, minBytes: 1 } }); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const zstded = await internals.compress('zstdCompress', Buffer.from(data)); + const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'zstd' }, payload: data }); + expect(payload.toString()).to.equal(zstded.toString()); + await server.stop(); + }); + + it('returns a zstd response on a get request when accept-encoding: zstd is requested', { skip: !Common.hasZstd }, async () => { + + const data = '{"test":"true"}'; + + const server = Hapi.server({ compression: { encodings: { zstd: true }, minBytes: 1 } }); + server.route({ method: 'GET', path: '/', handler: () => data }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const zstded = await internals.compress('zstdCompress', Buffer.from(data)); + const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'zstd' } }); + expect(payload.toString()).to.equal(zstded.toString()); + await server.stop(); + }); + + 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: { encodings: { br: true }, 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: { encodings: { br: true }, 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"}'; @@ -789,8 +908,9 @@ describe('transmission', () => { await server.start(); 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': '*' }, payload: data }); - expect(payload.toString()).to.equal(data); + expect(payload.toString()).to.equal(zipped.toString()); await server.stop(); }); @@ -802,8 +922,9 @@ describe('transmission', () => { await server.start(); 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': '*' } }); - expect(payload.toString()).to.equal(data); + expect(payload.toString()).to.equal(zipped.toString()); await server.stop(); }); @@ -835,7 +956,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; zstd;q=0.8 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -844,12 +965,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, zstd;q=0.8' }, 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, zstd;q=0.8 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -858,12 +979,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, zstd;q=0.8' } }); 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, zstd;q=0.8 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -872,12 +993,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, zstd;q=0.8' }, 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, zstd;q=0.8 is requested', async () => { const data = '{"test":"true"}'; const server = Hapi.server({ compression: { minBytes: 1 } }); @@ -886,11 +1007,67 @@ 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, zstd;q=0.8' } }); expect(payload.toString()).to.equal(deflated.toString()); await server.stop(); }); + it('returns a br response on a post request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=1, zstd;q=0.8 is requested', async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { br: true }, minBytes: 1, priority: ['br'] } }); + 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': 'gzip;q=1, deflate;q=0.5, br;q=1, zstd;q=0.8' }, payload: data }); + expect(payload.toString()).to.equal(brotlied.toString()); + await server.stop(); + }); + + it('returns a br response on a get request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=1, zstd;q=0.8 is requested', async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { br: true }, minBytes: 1, priority: ['br'] } }); + 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': 'gzip;q=1, deflate;q=0.5, br;q=1, zstd;q=0.8' } }); + expect(payload.toString()).to.equal(brotlied.toString()); + await server.stop(); + }); + + it('returns a zstd response on a post request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=0.7; zstd;q=1 is requested', { skip: !Common.hasZstd }, async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { zstd: true }, minBytes: 1, priority: ['zstd'] } }); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const zstded = await internals.compress('zstdCompress', Buffer.from(data)); + const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5, br;q=0.7, zstd;q=1' }, payload: data }); + expect(payload.toString()).to.equal(zstded.toString()); + await server.stop(); + }); + + it('returns a zstd response on a get request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=0.7, zstd;q=1 is requested', { skip: !Common.hasZstd }, async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { zstd: true }, minBytes: 1, priority: ['zstd'] } }); + server.route({ method: 'GET', path: '/', handler: () => data }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const zstded = await internals.compress('zstdCompress', Buffer.from(data)); + const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5, br;q=0.7, zstd;q=1' } }); + expect(payload.toString()).to.equal(zstded.toString()); + await server.stop(); + }); + it('returns a gzip response on a post request when accept-encoding: deflate, gzip is requested', async () => { const data = '{"test":"true"}'; @@ -919,6 +1096,62 @@ describe('transmission', () => { await server.stop(); }); + it('returns a br response on a post request when accept-encoding: gzip, deflate, br is requested', async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { br: true }, minBytes: 1, priority: ['br'] } }); + 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': 'gzip, deflate, 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: gzip, deflate, br is requested', async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { br: true }, minBytes: 1, priority: ['br'] } }); + 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': 'gzip, deflate, br' } }); + expect(payload.toString()).to.equal(brotlied.toString()); + await server.stop(); + }); + + it('returns a zstd response on a post request when accept-encoding: gzip, deflate, br, zstd is requested', { skip: !Common.hasZstd }, async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { zstd: true }, minBytes: 1, priority: ['zstd'] } }); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const zstded = await internals.compress('zstdCompress', Buffer.from(data)); + const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip, deflate, br, zstd' }, payload: data }); + expect(payload.toString()).to.equal(zstded.toString()); + await server.stop(); + }); + + it('returns a zstd response on a get request when accept-encoding: gzip, deflate, br, zstd is requested', { skip: !Common.hasZstd }, async () => { + + const data = '{"test":"true"}'; + const server = Hapi.server({ compression: { encodings: { zstd: true }, minBytes: 1, priority: ['zstd'] } }); + server.route({ method: 'GET', path: '/', handler: () => data }); + await server.start(); + + const uri = 'http://localhost:' + server.info.port; + const zstded = await internals.compress('zstdCompress', Buffer.from(data)); + const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip, deflate, br, zstd' } }); + expect(payload.toString()).to.equal(zstded.toString()); + await server.stop(); + }); + it('boom object reused does not affect encoding header.', async () => { const error = Boom.badRequest();