Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 448ad9e

Browse files
committedMay 19, 2024
feat: add brotli to supported compression 🗜️
1 parent 6068875 commit 448ad9e

File tree

8 files changed

+980
-676
lines changed

8 files changed

+980
-676
lines changed
 

‎API.md

+824-672
Large diffs are not rendered by default.

‎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)
Please sign in to comment.