Skip to content

Commit fbe22de

Browse files
committed
feat: add brotli to supported compression 🗜️
1 parent 6068875 commit fbe22de

File tree

8 files changed

+163
-4
lines changed

8 files changed

+163
-4
lines changed

API.md

+7
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ Default value: '1024'.
110110
Sets the minimum response payload size in bytes that is required for content encoding compression.
111111
If the payload size is under the limit, no compression is performed.
112112

113+
##### <a name="server.options.compression.priority" /> `server.options.compression.priority`
114+
115+
Default value: `null`.
116+
117+
Sets the priority for content encoding compression algorithms in descending order,
118+
e.g.: `['br', 'gzip', 'deflate']`.
119+
113120
#### <a name="server.options.debug" /> `server.options.debug`
114121

115122
Default value: `{ request: ['implementation'] }`.

lib/compression.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,23 @@ const Hoek = require('@hapi/hoek');
88

99

1010
const internals = {
11-
common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'gzip, deflate, br']
11+
common: ['gzip, deflate', 'deflate, gzip', 'gzip', 'deflate', 'br', 'gzip, deflate, br']
1212
};
1313

1414

1515
exports = module.exports = internals.Compression = class {
1616

1717
decoders = {
18+
br: (options) => Zlib.createBrotliDecompress(options),
1819
gzip: (options) => Zlib.createGunzip(options),
1920
deflate: (options) => Zlib.createInflate(options)
2021
};
2122

22-
encodings = ['identity', 'gzip', 'deflate'];
23+
encodings = ['identity', 'gzip', 'deflate', 'br'];
2324

2425
encoders = {
2526
identity: null,
27+
br: (options) => Zlib.createBrotliCompress(options),
2628
gzip: (options) => Zlib.createGzip(options),
2729
deflate: (options) => Zlib.createDeflate(options)
2830
};
@@ -116,4 +118,10 @@ exports = module.exports = internals.Compression = class {
116118
Hoek.assert(encoder !== undefined, `Unknown encoding ${encoding}`);
117119
return encoder(request.route.settings.compression[encoding]);
118120
}
121+
122+
setPriority(priority) {
123+
124+
this.encodings = [...new Set([...priority, ...this.encodings])];
125+
this._updateCommons();
126+
}
119127
};

lib/config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ internals.server = Validate.object({
241241
autoListen: Validate.boolean(),
242242
cache: Validate.allow(null), // Validated elsewhere
243243
compression: Validate.object({
244-
minBytes: Validate.number().min(1).integer().default(1024)
244+
minBytes: Validate.number().min(1).integer().default(1024),
245+
priority: Validate.array().items(Validate.string().valid('gzip', 'deflate', 'br')).default(null)
245246
})
246247
.allow(false)
247248
.default(),

lib/core.js

+4
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ exports = module.exports = internals.Core = class {
127127
this._debug();
128128
this._initializeCache();
129129

130+
if (this.settings.compression.priority) {
131+
this.compression.setPriority(this.settings.compression.priority);
132+
}
133+
130134
if (this.settings.routes.validate.validator) {
131135
this.validator = Validation.validator(this.settings.routes.validate.validator);
132136
}

lib/types/server/encoders.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createDeflate, createGunzip, createGzip, createInflate } from 'zlib';
1+
import { createBrotliCompress, createBrotliDecompress, createDeflate, createGunzip, createGzip, createInflate } from 'zlib';
22

33
/**
44
* Available [content encoders](https://github.com/hapijs/hapi/blob/master/API.md#-serverencoderencoding-encoder).
@@ -7,6 +7,7 @@ export interface ContentEncoders {
77

88
deflate: typeof createDeflate;
99
gzip: typeof createGzip;
10+
br: typeof createBrotliCompress;
1011
}
1112

1213
/**
@@ -16,4 +17,5 @@ export interface ContentDecoders {
1617

1718
deflate: typeof createInflate;
1819
gzip: typeof createGunzip;
20+
br: typeof createBrotliDecompress;
1921
}

lib/types/server/options.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SameSitePolicy } from './state';
1010

1111
export interface ServerOptionsCompression {
1212
minBytes: number;
13+
priority: string[];
1314
}
1415

1516
/**

test/payload.js

+24
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,30 @@ describe('Payload', () => {
407407
expect(res.result).to.equal(message);
408408
});
409409

410+
it('handles br payload', async () => {
411+
412+
const message = { 'msg': 'This message is going to be brotlied.' };
413+
const server = Hapi.server();
414+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
415+
416+
const compressed = await new Promise((resolve) => Zlib.brotliCompress(JSON.stringify(message), (ignore, result) => resolve(result)));
417+
418+
const request = {
419+
method: 'POST',
420+
url: '/',
421+
headers: {
422+
'content-type': 'application/json',
423+
'content-encoding': 'br',
424+
'content-length': compressed.length
425+
},
426+
payload: compressed
427+
};
428+
429+
const res = await server.inject(request);
430+
expect(res.result).to.exist();
431+
expect(res.result).to.equal(message);
432+
});
433+
410434
it('handles custom compression', async () => {
411435

412436
const message = { 'msg': 'This message is going to be gzipped.' };

test/transmit.js

+112
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,19 @@ describe('transmission', () => {
677677
expect(res3.headers['last-modified']).to.equal(res2.headers['last-modified']);
678678
});
679679

680+
it('returns a brotlied file in the response when the request accepts br', async () => {
681+
682+
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } });
683+
await server.register(Inert);
684+
server.route({ method: 'GET', path: '/file', handler: (request, h) => h.file(__dirname + '/../package.json') });
685+
686+
const res = await server.inject({ url: '/file', headers: { 'accept-encoding': 'br' } });
687+
expect(res.headers['content-type']).to.equal('application/json; charset=utf-8');
688+
expect(res.headers['content-encoding']).to.equal('br');
689+
expect(res.headers['content-length']).to.not.exist();
690+
expect(res.payload).to.exist();
691+
});
692+
680693
it('returns a gzipped file in the response when the request accepts gzip', async () => {
681694

682695
const server = Hapi.server({ compression: { minBytes: 1 }, routes: { files: { relativeTo: __dirname } } });
@@ -729,6 +742,16 @@ describe('transmission', () => {
729742
expect(res.payload).to.exist();
730743
});
731744

745+
it('returns a brotlied stream response without a content-length header when accept-encoding is br', async () => {
746+
747+
const server = Hapi.server({ compression: { minBytes: 1 } });
748+
server.route({ method: 'GET', path: '/stream', handler: () => new internals.TimerStream() });
749+
750+
const res = await server.inject({ url: '/stream', headers: { 'Content-Type': 'application/json', 'accept-encoding': 'br' } });
751+
expect(res.statusCode).to.equal(200);
752+
expect(res.headers['content-length']).to.not.exist();
753+
});
754+
732755
it('returns a gzipped stream response without a content-length header when accept-encoding is gzip', async () => {
733756

734757
const server = Hapi.server({ compression: { minBytes: 1 } });
@@ -749,6 +772,37 @@ describe('transmission', () => {
749772
expect(res.headers['content-length']).to.not.exist();
750773
});
751774

775+
it('returns a br response on a post request when accept-encoding: br is requested', async () => {
776+
777+
const data = '{"test":"true"}';
778+
779+
const server = Hapi.server({ compression: { minBytes: 1 } });
780+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
781+
await server.start();
782+
783+
const uri = 'http://localhost:' + server.info.port;
784+
const brotlied = await internals.compress('brotliCompress', Buffer.from(data));
785+
786+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'br' }, payload: data });
787+
expect(payload.toString()).to.equal(brotlied.toString());
788+
await server.stop();
789+
});
790+
791+
it('returns a br response on a get request when accept-encoding: br is requested', async () => {
792+
793+
const data = '{"test":"true"}';
794+
795+
const server = Hapi.server({ compression: { minBytes: 1 } });
796+
server.route({ method: 'GET', path: '/', handler: () => data });
797+
await server.start();
798+
799+
const uri = 'http://localhost:' + server.info.port;
800+
const brotlied = await internals.compress('brotliCompress', Buffer.from(data));
801+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'br' } });
802+
expect(payload.toString()).to.equal(brotlied.toString());
803+
await server.stop();
804+
});
805+
752806
it('returns a gzip response on a post request when accept-encoding: gzip is requested', async () => {
753807

754808
const data = '{"test":"true"}';
@@ -891,6 +945,35 @@ describe('transmission', () => {
891945
await server.stop();
892946
});
893947

948+
949+
it('returns a br response on a post request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=1 is requested', async () => {
950+
951+
const data = '{"test":"true"}';
952+
const server = Hapi.server({ compression: { minBytes: 1, priority: ['br'] } });
953+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
954+
await server.start();
955+
956+
const uri = 'http://localhost:' + server.info.port;
957+
const brotlied = await internals.compress('brotliCompress', Buffer.from(data));
958+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5, br;q=1' }, payload: data });
959+
expect(payload.toString()).to.equal(brotlied.toString());
960+
await server.stop();
961+
});
962+
963+
it('returns a br response on a get request when accept-encoding: gzip;q=1, deflate;q=0.5, br;q=1 is requested', async () => {
964+
965+
const data = '{"test":"true"}';
966+
const server = Hapi.server({ compression: { minBytes: 1, priority: ['br'] } });
967+
server.route({ method: 'GET', path: '/', handler: () => data });
968+
await server.start();
969+
970+
const uri = 'http://localhost:' + server.info.port;
971+
const brotlied = await internals.compress('brotliCompress', Buffer.from(data));
972+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip;q=1, deflate;q=0.5, br;q=1' } });
973+
expect(payload.toString()).to.equal(brotlied.toString());
974+
await server.stop();
975+
});
976+
894977
it('returns a gzip response on a post request when accept-encoding: deflate, gzip is requested', async () => {
895978

896979
const data = '{"test":"true"}';
@@ -919,6 +1002,35 @@ describe('transmission', () => {
9191002
await server.stop();
9201003
});
9211004

1005+
1006+
it('returns a br response on a post request when accept-encoding: gzip, deflate, br is requested', async () => {
1007+
1008+
const data = '{"test":"true"}';
1009+
const server = Hapi.server({ compression: { minBytes: 1, priority: ['br'] } });
1010+
server.route({ method: 'POST', path: '/', handler: (request) => request.payload });
1011+
await server.start();
1012+
1013+
const uri = 'http://localhost:' + server.info.port;
1014+
const brotlied = await internals.compress('brotliCompress', Buffer.from(data));
1015+
const { payload } = await Wreck.post(uri, { headers: { 'accept-encoding': 'gzip, deflate, br' }, payload: data });
1016+
expect(payload.toString()).to.equal(brotlied.toString());
1017+
await server.stop();
1018+
});
1019+
1020+
it('returns a br response on a get request when accept-encoding: gzip, deflate, br is requested', async () => {
1021+
1022+
const data = '{"test":"true"}';
1023+
const server = Hapi.server({ compression: { minBytes: 1, priority: ['br'] } });
1024+
server.route({ method: 'GET', path: '/', handler: () => data });
1025+
await server.start();
1026+
1027+
const uri = 'http://localhost:' + server.info.port;
1028+
const brotlied = await internals.compress('brotliCompress', Buffer.from(data));
1029+
const { payload } = await Wreck.get(uri, { headers: { 'accept-encoding': 'gzip, deflate, br' } });
1030+
expect(payload.toString()).to.equal(brotlied.toString());
1031+
await server.stop();
1032+
});
1033+
9221034
it('boom object reused does not affect encoding header.', async () => {
9231035

9241036
const error = Boom.badRequest();

0 commit comments

Comments
 (0)