Skip to content

Commit b91c1fa

Browse files
tigtEomm
authored andcommitted
feature: Allow encodings order (#84)
1 parent 2ac7c23 commit b91c1fa

File tree

6 files changed

+115
-33
lines changed

6 files changed

+115
-33
lines changed

README.md

+34-22
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,41 @@
44

55
[![Build Status](https://travis-ci.org/fastify/fastify-compress.svg?branch=master)](https://travis-ci.org/fastify/fastify-compress) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/)
66

7-
Adds compression utils to the Fastify `reply` object.
8-
Support `gzip`, `deflate` and `brotli`.
7+
Adds compression utils to [the Fastify `reply` object](https://www.fastify.io/docs/master/Reply/).
8+
Supports `gzip`, `deflate`, and `brotli`.
99

1010
## Install
1111
```
12-
npm i fastify-compress --save
12+
npm i fastify-compress
1313
```
1414

1515
## Usage
16-
This plugins adds two functionalities to Fastify, a compress utility and a global compression hook.
16+
This plugin adds two functionalities to Fastify: a compress utility and a global compression hook.
1717

18-
Currently the following headers are supported:
19-
- `'deflate'`
20-
- `'gzip'`
21-
- `'br'`
22-
- `'*'`
18+
Currently, the following encoding tokens are supported, using the first acceptable token in this order:
2319

24-
If the `'accept-encoding'` header specifies no preferred encoding with an asterisk `*` the payload will be compressed with `gzip`.
20+
1. `br`
21+
2. `gzip`
22+
3. `deflate`
23+
4. `*` (no preference — `fastify-compress` will use `gzip`)
24+
5. `identity` (no compression)
2525

26-
If an unsupported encoding is received or if the `'accept-encoding'` header is missing, it will not compress the payload.
26+
If the `accept-encoding` header is missing or contains no supported encodings, it will not compress the payload.
2727

28-
It automatically defines if a payload should be compressed or not based on its `Content-Type`, if no content type is present, it will assume is `application/json`.
28+
The plugin automatically decides if a payload should be compressed based on its `content-type`; if no content type is present, it will assume `application/json`.
2929

3030
### Global hook
31-
The global compression hook is enabled by default if you want to disable it, pass the option `{ global: false }`.
31+
The global compression hook is enabled by default. To disable it, pass the option `{ global: false }`:
3232
```javascript
3333
fastify.register(
3434
require('fastify-compress'),
3535
{ global: false }
3636
)
3737
```
38-
Remember that thanks to the Fastify encapsulation model, you can set a global compression, but running it only in a subset of routes is you wrap them inside a plugin.
38+
Remember that thanks to the Fastify encapsulation model, you can set a global compression, but run it only in a subset of routes if you wrap them inside a plugin.
3939

4040
### `reply.compress`
41-
This plugin add a `compress` function to `reply` that accepts a stream or a string and compress it based on the `'accept-encoding'` header. If a js object is passed in, will be stringified as json.
41+
This plugin adds a `compress` method to `reply` that accepts a stream or a string, and compresses it based on the `accept-encoding` header. If a JS object is passed in, it will be stringified to JSON.
4242

4343
```javascript
4444
const fs = require('fs')
@@ -59,25 +59,25 @@ fastify.listen(3000, function (err) {
5959
```
6060
## Options
6161
### Threshold
62-
You can set a custom threshold below which it will not be made compression, default to `1024`.
62+
The minimum byte size for a response to be compressed. Defaults to `1024`.
6363
```javascript
6464
fastify.register(
6565
require('fastify-compress'),
6666
{ threshold: 2048 }
6767
)
6868
```
6969
### customTypes
70-
[mime-db](https://github.com/jshttp/mime-db) is used to determine if a `Content-Type` should be compressed. You can compress additional content types via regular expression.
70+
[mime-db](https://github.com/jshttp/mime-db) is used to determine if a `content-type` should be compressed. You can compress additional content types via regular expression.
7171
```javascript
7272
fastify.register(
7373
require('fastify-compress'),
7474
{ customTypes: /x-protobuf$/ }
7575
)
7676
```
7777
### Brotli
78-
Brotli compression is enabled by default if your Node.js(>= v11.7.0) supports it natively.
78+
Brotli compression is enabled by default if your Node.js supports it natively (≥ v11.7.0).
7979

80-
For the Node.js versions that not support brotli natively, it's not enabled by default, if you need it we recommend to install [`iltorb`](https://www.npmjs.com/package/iltorb) and pass it as option.
80+
For Node.js versions that don’t natively support Brotli, it's not enabled by default. If you need it, we recommend installing [`iltorb`](https://www.npmjs.com/package/iltorb) and passing it to the `brotli` option:
8181

8282
```javascript
8383
fastify.register(
@@ -87,10 +87,10 @@ fastify.register(
8787
```
8888

8989
### Disable compression by header
90-
You can selectively disable the response compression by using the `x-no-compression` header in the request.
90+
You can selectively disable response compression by using the `x-no-compression` header in the request.
9191

9292
### Inflate pre-compressed bodies for clients that do not support compression
93-
Optional feature to inflate pre-compressed data if the client doesn't include one of the supported compression types in its `Accept-Encoding` header.
93+
Optional feature to inflate pre-compressed data if the client doesn't include one of the supported compression types in its `accept-encoding` header.
9494
```javascript
9595
fastify.register(
9696
require('fastify-compress'),
@@ -103,8 +103,20 @@ fastify.get('/file', (req, reply) =>
103103
reply.send(fs.createReadStream('./file.gz')))
104104
```
105105

106+
### Customize encoding priority
107+
108+
By default, `fastify-compress` prioritizes compression as described [at the beginning of §Usage](#usage). You can change that by passing an array of compression tokens to the `encodings` option:
109+
110+
```javascript
111+
fastify.register(
112+
require('fastify-compress'),
113+
// Only support gzip and deflate, and prefer deflate to gzip
114+
{ encodings: ['deflate', 'gzip'] }
115+
)
116+
```
117+
106118
## Note
107-
Please have in mind that in large scale scenarios, you should use a proxy like Nginx to handle response-compression.
119+
Please note that in large-scale scenarios, you should use a proxy like Nginx to handle response compression.
108120

109121
## Acknowledgements
110122
This project is kindly sponsored by:

index.d.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Plugin } from 'fastify'
22
import { Server, IncomingMessage, ServerResponse } from 'http'
33

4+
type EncodingToken = 'br' | 'deflate' | 'gzip' | 'identity'
5+
46
declare const fastifyCompress: Plugin<
57
Server,
68
IncomingMessage,
@@ -12,6 +14,7 @@ declare const fastifyCompress: Plugin<
1214
brotli?: NodeModule
1315
zlib?: NodeModule
1416
inflateIfDeflated?: boolean
17+
encodings?: Array<EncodingToken>
1518
}
1619
>
1720

index.js

+28-8
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,24 @@ function compressPlugin (fastify, opts, next) {
3737
const supportedEncodings = ['gzip', 'deflate', 'identity']
3838
if (opts.brotli) {
3939
compressStream.br = opts.brotli.compressStream
40-
supportedEncodings.push('br')
40+
supportedEncodings.unshift('br')
4141
} else if (zlib.createBrotliCompress) {
4242
compressStream.br = zlib.createBrotliCompress
43-
supportedEncodings.push('br')
43+
supportedEncodings.unshift('br')
44+
}
45+
46+
if (opts.encodings && opts.encodings.length < 1) {
47+
next(new Error('The `encodings` option array must have at least 1 item.'))
48+
}
49+
50+
const encodings = Array.isArray(opts.encodings)
51+
? supportedEncodings
52+
.filter(encoding => opts.encodings.includes(encoding))
53+
.sort((a, b) => opts.encodings.indexOf(a) - supportedEncodings.indexOf(b))
54+
: supportedEncodings
55+
56+
if (encodings.length < 1) {
57+
next(new Error('None of the passed `encodings` were supported — compression not possible.'))
4458
}
4559

4660
next()
@@ -56,10 +70,10 @@ function compressPlugin (fastify, opts, next) {
5670
var noCompress =
5771
// don't compress on x-no-compression header
5872
(this.request.headers['x-no-compression'] !== undefined) ||
59-
// don't compress if not one of the indiated compressible types
73+
// don't compress if not one of the indicated compressible types
6074
(shouldCompress(this.getHeader('Content-Type') || 'application/json', compressibleTypes) === false) ||
6175
// don't compress on missing or identity `accept-encoding` header
62-
((encoding = getEncodingHeader(supportedEncodings, this.request)) == null || encoding === 'identity')
76+
((encoding = getEncodingHeader(encodings, this.request)) == null || encoding === 'identity')
6377

6478
if (noCompress) {
6579
if (inflateIfDeflated && isStream(stream = maybeUnzip(payload, this.serialize.bind(this)))) {
@@ -106,7 +120,7 @@ function compressPlugin (fastify, opts, next) {
106120
// don't compress if not one of the indiated compressible types
107121
(shouldCompress(reply.getHeader('Content-Type') || 'application/json', compressibleTypes) === false) ||
108122
// don't compress on missing or identity `accept-encoding` header
109-
((encoding = getEncodingHeader(supportedEncodings, req)) == null || encoding === 'identity')
123+
((encoding = getEncodingHeader(encodings, req)) == null || encoding === 'identity')
110124

111125
if (noCompress) {
112126
if (inflateIfDeflated && isStream(stream = maybeUnzip(payload))) {
@@ -139,9 +153,15 @@ function onEnd (err) {
139153
if (err) this.res.log.error(err)
140154
}
141155

142-
function getEncodingHeader (supportedEncodings, request) {
143-
const header = request.headers['accept-encoding']
144-
return header == null ? undefined : encodingNegotiator.negotiate(header.toLowerCase(), supportedEncodings)
156+
function getEncodingHeader (encodings, request) {
157+
let header = request.headers['accept-encoding']
158+
if (header != null) {
159+
header = header.toLowerCase()
160+
.replace('*', 'gzip') // consider the no-preference token as gzip for downstream compat
161+
return encodingNegotiator.negotiate(header, encodings)
162+
} else {
163+
return undefined
164+
}
145165
}
146166

147167
function shouldCompress (type, compressibleTypes) {

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"compression",
4545
"deflate",
4646
"gzip",
47-
"brodli"
47+
"brotli"
4848
],
4949
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
5050
"license": "MIT",

test/test.js

+47-1
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ test('should follow the encoding order', t => {
282282
url: '/',
283283
method: 'GET',
284284
headers: {
285-
'accept-encoding': 'hello,br'
285+
'accept-encoding': 'hello,br,gzip'
286286
}
287287
}, (err, res) => {
288288
t.error(err)
@@ -1443,3 +1443,49 @@ test('Should not apply customTypes if value passed is not RegExp', t => {
14431443
t.strictEqual(res.statusCode, 200)
14441444
})
14451445
})
1446+
1447+
test('Should only use `encodings` if passed', t => {
1448+
t.plan(3)
1449+
const fastify = Fastify()
1450+
fastify.register(compressPlugin, { encodings: ['deflate'] })
1451+
1452+
fastify.get('/', (req, reply) => {
1453+
reply.send(createReadStream('./package.json'))
1454+
})
1455+
1456+
fastify.inject({
1457+
url: '/',
1458+
method: 'GET',
1459+
headers: {
1460+
'accept-encoding': 'br,gzip,deflate'
1461+
}
1462+
}, (err, res) => {
1463+
t.error(err)
1464+
t.strictEqual(res.headers['content-encoding'], 'deflate')
1465+
t.strictEqual(res.statusCode, 200)
1466+
})
1467+
})
1468+
1469+
test('Should error if `encodings` array is empty', t => {
1470+
t.plan(1)
1471+
const fastify = Fastify()
1472+
1473+
fastify.register(compressPlugin, { encodings: [] })
1474+
1475+
fastify.ready(err => {
1476+
t.ok(err instanceof Error)
1477+
})
1478+
})
1479+
1480+
test('Should error if no entries in `encodings` are supported', t => {
1481+
t.plan(1)
1482+
const fastify = Fastify()
1483+
1484+
fastify.register(compressPlugin, {
1485+
encodings: ['(not-a-real-encoding)']
1486+
})
1487+
1488+
fastify.ready(err => {
1489+
t.ok(err instanceof Error)
1490+
})
1491+
})

test/types/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ app.register(fastifyCompress, {
1212
brotli: iltorb,
1313
zlib: zlib,
1414
inflateIfDeflated: true,
15-
customTypes: /x-protobuf$/
15+
customTypes: /x-protobuf$/,
16+
encodings: ['gzip', 'br', 'identity', 'deflate']
1617
})

0 commit comments

Comments
 (0)