Skip to content

Commit

Permalink
feat: add brotli to supported compression 🗜️
Browse files Browse the repository at this point in the history
  • Loading branch information
bricss committed May 19, 2024
1 parent 6068875 commit 448ad9e
Show file tree
Hide file tree
Showing 8 changed files with 980 additions and 676 deletions.
1,496 changes: 824 additions & 672 deletions API.md

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions lib/compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down Expand Up @@ -116,4 +118,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();
}
};
3 changes: 2 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ internals.server = Validate.object({
autoListen: Validate.boolean(),
cache: Validate.allow(null), // Validated elsewhere
compression: Validate.object({
minBytes: Validate.number().min(1).integer().default(1024)
minBytes: Validate.number().min(1).integer().default(1024),
priority: Validate.array().items(Validate.string().valid('gzip', 'deflate', 'br')).default(null)
})
.allow(false)
.default(),
Expand Down
4 changes: 4 additions & 0 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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);
}
Expand Down
4 changes: 3 additions & 1 deletion lib/types/server/encoders.d.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -7,6 +7,7 @@ export interface ContentEncoders {

deflate: typeof createDeflate;
gzip: typeof createGzip;
br: typeof createBrotliCompress;
}

/**
Expand All @@ -16,4 +17,5 @@ export interface ContentDecoders {

deflate: typeof createInflate;
gzip: typeof createGunzip;
br: typeof createBrotliDecompress;
}
1 change: 1 addition & 0 deletions lib/types/server/options.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SameSitePolicy } from './state';

export interface ServerOptionsCompression {
minBytes: number;
priority: string[];
}

/**
Expand Down
24 changes: 24 additions & 0 deletions test/payload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.' };
Expand Down
112 changes: 112 additions & 0 deletions test/transmit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } } });
Expand Down Expand Up @@ -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 } });
Expand All @@ -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"}';
Expand Down Expand Up @@ -891,6 +945,35 @@ describe('transmission', () => {
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 is requested', async () => {

const data = '{"test":"true"}';
const server = Hapi.server({ compression: { 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' }, 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 is requested', async () => {

const data = '{"test":"true"}';
const server = Hapi.server({ compression: { 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' } });
expect(payload.toString()).to.equal(brotlied.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"}';
Expand Down Expand Up @@ -919,6 +1002,35 @@ 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: { 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: { 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('boom object reused does not affect encoding header.', async () => {

const error = Boom.badRequest();
Expand Down

0 comments on commit 448ad9e

Please sign in to comment.