├── .c8rc ├── .gitattributes ├── .github ├── dependabot.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.js ├── index.js ├── lib └── utils.js ├── package.json ├── test ├── global-compress.test.js ├── global-decompress.test.js ├── regression │ └── issue-288.test.js ├── routes-compress.test.js ├── routes-decompress.test.js └── utils.test.js └── types ├── index.d.ts └── index.test-d.ts /.c8rc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "exclude": [ 4 | "test/", 5 | "coverage/", 6 | "types/" 7 | ], 8 | "clean": true, 9 | "check-coverage": true, 10 | "branches": 100, 11 | "lines": 100, 12 | "functions": 100, 13 | "statements": 100 14 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically convert line endings 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | - 'v*' 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | pull_request: 13 | paths-ignore: 14 | - 'docs/**' 15 | - '*.md' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | test: 22 | permissions: 23 | contents: write 24 | pull-requests: write 25 | uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 26 | with: 27 | license-check: true 28 | lint: true 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # Clinic 139 | .clinic 140 | 141 | # lock files 142 | bun.lockb 143 | package-lock.json 144 | pnpm-lock.yaml 145 | yarn.lock 146 | 147 | # editor files 148 | .vscode 149 | .idea 150 | 151 | #tap files 152 | .tap/ 153 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fastify 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @fastify/compress 2 | 3 | [![CI](https://github.com/fastify/fastify-compress/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-compress/actions/workflows/ci.yml) 4 | [![NPM version](https://img.shields.io/npm/v/@fastify/compress.svg?style=flat)](https://www.npmjs.com/package/@fastify/compress) 5 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 6 | 7 | Adds compression utils to [the Fastify `reply` object](https://fastify.dev/docs/latest/Reference/Reply/#reply) and a hook to decompress requests payloads. 8 | Supports `gzip`, `deflate`, and `brotli`. 9 | 10 | > ℹ️ Note: In large-scale scenarios, use a proxy like Nginx to handle response compression. 11 | 12 | > ⚠ Warning: Since `@fastify/compress` version 4.x, payloads compressed with the `zip` algorithm are not automatically uncompressed. This plugin focuses on response compression, and `zip` is not in the [IANA Table of Content Encodings](https://www.iana.org/assignments/http-parameters/http-parameters.xml#content-coding). 13 | 14 | ## Install 15 | ``` 16 | npm i @fastify/compress 17 | ``` 18 | 19 | ### Compatibility 20 | | Plugin version | Fastify version | 21 | | ---------------|-----------------| 22 | | `>=8.x` | `^5.x` | 23 | | `>=6.x <8.x` | `^4.x` | 24 | | `>=3.x <6.x` | `^3.x` | 25 | | `^2.x` | `^2.x` | 26 | | `>=0.x <2.x` | `^1.x` | 27 | 28 | 29 | Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin 30 | in the table above. 31 | See [Fastify's LTS policy](https://github.com/fastify/fastify/blob/main/docs/Reference/LTS.md) for more details. 32 | 33 | 34 | ## Usage - Compress replies 35 | 36 | This plugin adds two functionalities to Fastify: a compress utility and a global compression hook. 37 | 38 | Currently, the following encoding tokens are supported, using the first acceptable token in this order: 39 | 40 | 1. `br` 41 | 2. `gzip` 42 | 3. `deflate` 43 | 4. `*` (no preference — `@fastify/compress` will use `gzip`) 44 | 5. `identity` (no compression) 45 | 46 | If an unsupported encoding is received or the `'accept-encoding'` header is missing, the payload will not be compressed. 47 | To return an error for unsupported encoding, use the `onUnsupportedEncoding` option. 48 | 49 | The plugin compresses payloads based on `content-type`. If absent, it assumes `application/json`. 50 | 51 | ### Global hook 52 | The global compression hook is enabled by default. To disable it, pass `{ global: false }`: 53 | ```js 54 | await fastify.register( 55 | import('@fastify/compress'), 56 | { global: false } 57 | ) 58 | ``` 59 | Fastify encapsulation can be used to set global compression but run it only in a subset of routes by wrapping them inside a plugin. 60 | 61 | > ℹ️ Note: If using `@fastify/compress` plugin together with `@fastify/static` plugin, `@fastify/compress` must be registered (with *global hook*) **before** registering `@fastify/static`. 62 | 63 | ### Per Route options 64 | Different compression options can be specified per route using the `compress` options in the route's configuration. 65 | Setting `compress: false` on any route will disable compression on the route even if global compression is enabled. 66 | ```js 67 | await fastify.register( 68 | import('@fastify/compress'), 69 | { global: false } 70 | ) 71 | 72 | // only compress if the payload is above a certain size and use brotli 73 | fastify.get('/custom-route', { 74 | compress: { 75 | inflateIfDeflated: true, 76 | threshold: 128, 77 | zlib: { 78 | createBrotliCompress: () => createYourCustomBrotliCompress(), 79 | createGzip: () => createYourCustomGzip(), 80 | createDeflate: () => createYourCustomDeflate() 81 | } 82 | }, (req, reply) => { 83 | // ... 84 | }) 85 | ``` 86 | 87 | ### `reply.compress` 88 | This plugin adds a `compress` method to `reply` that compresses a stream or string based on the `accept-encoding` header. If a JS object is passed, it will be stringified to JSON. 89 | 90 | The `compress` method uses per-route parameters if configured, otherwise it uses global parameters. 91 | 92 | ```js 93 | import fs from 'node:fs' 94 | import fastify from 'fastify' 95 | 96 | const app = fastify() 97 | await app.register(import('@fastify/compress'), { global: false }) 98 | 99 | app.get('/', (req, reply) => { 100 | reply 101 | .type('text/plain') 102 | .compress(fs.createReadStream('./package.json')) 103 | }) 104 | 105 | await app.listen({ port: 3000 }) 106 | ``` 107 | 108 | ## Compress Options 109 | 110 | ### threshold 111 | The minimum byte size for response compression. Defaults to `1024`. 112 | ```js 113 | await fastify.register( 114 | import('@fastify/compress'), 115 | { threshold: 2048 } 116 | ) 117 | ``` 118 | ### customTypes 119 | [mime-db](https://github.com/jshttp/mime-db) determines if a `content-type` should be compressed. Additional content types can be compressed via regex or a function. 120 | 121 | ```js 122 | await fastify.register( 123 | import('@fastify/compress'), 124 | { customTypes: /x-protobuf$/ } 125 | ) 126 | ``` 127 | 128 | or 129 | 130 | ```js 131 | await fastify.register( 132 | import('@fastify/compress'), 133 | { customTypes: contentType => contentType.endsWith('x-protobuf') } 134 | ) 135 | ``` 136 | 137 | ### onUnsupportedEncoding 138 | Set `onUnsupportedEncoding(encoding, request, reply)` to send a custom error response for unsupported encoding. The function can modify the reply and return a `string | Buffer | Stream | Error` payload. 139 | 140 | ```js 141 | await fastify.register( 142 | import('@fastify/compress'), 143 | { 144 | onUnsupportedEncoding: (encoding, request, reply) => { 145 | reply.code(406) 146 | return 'We do not support the ' + encoding + ' encoding.' 147 | } 148 | } 149 | ) 150 | ``` 151 | 152 | ### Disable compression by header 153 | Response compression can be disabled by an `x-no-compression` header in the request. 154 | 155 | ### Inflate pre-compressed bodies for clients that do not support compression 156 | Optional feature to inflate pre-compressed data if the client does not include one of the supported compression types in its `accept-encoding` header. 157 | ```js 158 | await fastify.register( 159 | import('@fastify/compress'), 160 | { inflateIfDeflated: true } 161 | ) 162 | 163 | fastify.get('/file', (req, reply) => 164 | // will inflate the file on the way out for clients 165 | // that indicate they do not support compression 166 | reply.send(fs.createReadStream('./file.gz'))) 167 | ``` 168 | 169 | ### Customize encoding priority 170 | By default, `@fastify/compress` prioritizes compression as described [here](#usage). Change this by passing an array of compression tokens to the `encodings` option: 171 | 172 | ```js 173 | await fastify.register( 174 | import('@fastify/compress'), 175 | // Only support gzip and deflate, and prefer deflate to gzip 176 | { encodings: ['deflate', 'gzip'] } 177 | ) 178 | ``` 179 | 180 | ### brotliOptions and zlibOptions 181 | Compression can be tuned with `brotliOptions` and `zlibOptions`, which are passed directly to native node `zlib` methods. See [class definitions](https://nodejs.org/api/zlib.html#zlib_class_options). 182 | 183 | ```js 184 | server.register(fastifyCompress, { 185 | brotliOptions: { 186 | params: { 187 | [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, // useful for APIs that primarily return text 188 | [zlib.constants.BROTLI_PARAM_QUALITY]: 4, // default is 4, max is 11, min is 0 189 | }, 190 | }, 191 | zlibOptions: { 192 | level: 6, // default is typically 6, max is 9, min is 0 193 | } 194 | }); 195 | ``` 196 | 197 | ### Manage `Content-Length` header removal with removeContentLengthHeader 198 | By default, `@fastify/compress` removes the reply `Content-Length` header. Change this by setting `removeContentLengthHeader` to `false` globally or per route. 199 | 200 | ```js 201 | // Global plugin scope 202 | await server.register(fastifyCompress, { global: true, removeContentLengthHeader: false }); 203 | 204 | // Route-specific scope 205 | fastify.get('/file', { 206 | compress: { removeContentLengthHeader: false } 207 | }, (req, reply) => 208 | reply.compress(fs.createReadStream('./file.gz')) 209 | ) 210 | ``` 211 | 212 | ## Usage - Decompress request payloads 213 | This plugin adds a `preParsing` hook to decompress the request payload based on the `content-encoding` request header. 214 | 215 | Currently, the following encoding tokens are supported: 216 | 217 | 1. `br` 218 | 2. `gzip` 219 | 3. `deflate` 220 | 221 | If an unsupported encoding or invalid payload is received, the plugin throws an error. 222 | 223 | If the request header is missing, the plugin yields to the next hook. 224 | 225 | ### Global hook 226 | 227 | The global request decompression hook is enabled by default. To disable it, pass `{ global: false }`: 228 | ```js 229 | await fastify.register( 230 | import('@fastify/compress'), 231 | { global: false } 232 | ) 233 | ``` 234 | 235 | Fastify encapsulation can be used to set global decompression but run it only in a subset of routes by wrapping them inside a plugin. 236 | 237 | ### Per Route options 238 | 239 | Specify different decompression options per route using the `decompress` options in the route's configuration. 240 | ```js 241 | await fastify.register( 242 | import('@fastify/compress'), 243 | { global: false } 244 | ) 245 | 246 | // Always decompress using gzip 247 | fastify.get('/custom-route', { 248 | decompress: { 249 | forceRequestEncoding: 'gzip', 250 | zlib: { 251 | createBrotliDecompress: () => createYourCustomBrotliDecompress(), 252 | createGunzip: () => createYourCustomGunzip(), 253 | createInflate: () => createYourCustomInflate() 254 | } 255 | } 256 | }, (req, reply) => { 257 | // ... 258 | }) 259 | ``` 260 | 261 | ### requestEncodings 262 | 263 | By default, `@fastify/compress` accepts all encodings specified [here](#usage). Change this by passing an array of compression tokens to the `requestEncodings` option: 264 | 265 | ```js 266 | await fastify.register( 267 | import('@fastify/compress'), 268 | // Only support gzip 269 | { requestEncodings: ['gzip'] } 270 | ) 271 | ``` 272 | 273 | ### forceRequestEncoding 274 | 275 | By default, `@fastify/compress` chooses the decompression algorithm based on the `content-encoding` header. 276 | 277 | One algorithm can be forced, and the header ignored, by providing the `forceRequestEncoding` option. 278 | 279 | If the request payload is not compressed, `@fastify/compress` will try to decompress, resulting in an error. 280 | 281 | ### onUnsupportedRequestEncoding 282 | 283 | The response error can be customized for unsupported request payload encoding by setting `onUnsupportedEncoding(request, encoding)` to a function that returns an error. 284 | 285 | ```js 286 | await fastify.register( 287 | import('@fastify/compress'), 288 | { 289 |  onUnsupportedRequestEncoding: (request, encoding) => { 290 | return { 291 | statusCode: 415, 292 | code: 'UNSUPPORTED', 293 | error: 'Unsupported Media Type', 294 | message: 'We do not support the ' + encoding + ' encoding.' 295 | } 296 | } 297 | } 298 | ) 299 | ``` 300 | 301 | ### onInvalidRequestPayload 302 | 303 | The response error can be customized for undetectable request payloads by setting `onInvalidRequestPayload(request, encoding)` to a function that returns an error. 304 | 305 | ```js 306 | await fastify.register( 307 | import('@fastify/compress'), 308 | { 309 | onInvalidRequestPayload: (request, encoding, error) => { 310 | return { 311 | statusCode: 400, 312 | code: 'BAD_REQUEST', 313 | error: 'Bad Request', 314 | message: 'This is not a valid ' + encoding + ' encoded payload: ' + error.message 315 | } 316 | } 317 | } 318 | ) 319 | ``` 320 | 321 | ## Acknowledgments 322 | 323 | Past sponsors: 324 | 325 | - [LetzDoIt](http://www.letzdoitapp.com/) 326 | 327 | ## License 328 | 329 | Licensed under [MIT](./LICENSE). 330 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ignores: require('neostandard').resolveIgnoresFromGitignore(), 5 | ts: true 6 | }) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const zlib = require('node:zlib') 4 | const { inherits, format } = require('node:util') 5 | 6 | const fp = require('fastify-plugin') 7 | const encodingNegotiator = require('@fastify/accept-negotiator') 8 | const pump = require('pump') 9 | const mimedb = require('mime-db') 10 | const peek = require('peek-stream') 11 | const { Minipass } = require('minipass') 12 | const pumpify = require('pumpify') 13 | const { Readable } = require('readable-stream') 14 | 15 | const { isStream, isGzip, isDeflate, intoAsyncIterator } = require('./lib/utils') 16 | 17 | const InvalidRequestEncodingError = createError('FST_CP_ERR_INVALID_CONTENT_ENCODING', 'Unsupported Content-Encoding: %s', 415) 18 | const InvalidRequestCompressedPayloadError = createError('FST_CP_ERR_INVALID_CONTENT', 'Could not decompress the request payload using the provided encoding', 400) 19 | 20 | function fastifyCompress (fastify, opts, next) { 21 | const globalCompressParams = processCompressParams(opts) 22 | const globalDecompressParams = processDecompressParams(opts) 23 | 24 | if (opts.encodings && opts.encodings.length < 1) { 25 | next(new Error('The `encodings` option array must have at least 1 item.')) 26 | return 27 | } 28 | 29 | if (opts.requestEncodings && opts.requestEncodings.length < 1) { 30 | next(new Error('The `requestEncodings` option array must have at least 1 item.')) 31 | return 32 | } 33 | 34 | if (globalCompressParams.encodings.length < 1) { 35 | next(new Error('None of the passed `encodings` were supported — compression not possible.')) 36 | return 37 | } 38 | 39 | if (globalDecompressParams.encodings.length < 1) { 40 | next(new Error('None of the passed `requestEncodings` were supported — request decompression not possible.')) 41 | return 42 | } 43 | 44 | if (globalDecompressParams.forceEncoding && !globalDecompressParams.encodings.includes(globalDecompressParams.forceEncoding)) { 45 | next(new Error(`Unsupported decompression encoding ${opts.forceRequestEncoding}.`)) 46 | return 47 | } 48 | 49 | fastify.decorateReply('compress', null) 50 | 51 | // add onSend hook onto each route as needed 52 | fastify.addHook('onRoute', (routeOptions) => { 53 | // If route config.compress has been set it takes precedence over compress 54 | if (routeOptions.config?.compress !== undefined) { 55 | routeOptions.compress = routeOptions.config.compress 56 | } 57 | 58 | // Manage compression options 59 | if (routeOptions.compress !== undefined) { 60 | if (typeof routeOptions.compress === 'object') { 61 | const mergedCompressParams = Object.assign( 62 | {}, globalCompressParams, processCompressParams(routeOptions.compress) 63 | ) 64 | 65 | // if the current endpoint has a custom compress configuration ... 66 | buildRouteCompress(fastify, mergedCompressParams, routeOptions) 67 | } else if (routeOptions.compress === false) { 68 | // don't apply any compress settings 69 | } else { 70 | throw new Error('Unknown value for route compress configuration') 71 | } 72 | } else if (globalCompressParams.global) { 73 | // if the plugin is set globally (meaning that all the routes will be compressed) 74 | // As the endpoint, does not have a custom rateLimit configuration, use the global one. 75 | buildRouteCompress(fastify, globalCompressParams, routeOptions) 76 | } else { 77 | // if no options are specified and the plugin is not global, then we still want to decorate 78 | // the reply in this case 79 | buildRouteCompress(fastify, globalCompressParams, routeOptions, true) 80 | } 81 | 82 | // If route config.decompress has been set it takes precedence over compress 83 | if (routeOptions.config?.decompress !== undefined) { 84 | routeOptions.decompress = routeOptions.config.decompress 85 | } 86 | 87 | // Manage decompression options 88 | if (routeOptions.decompress !== undefined) { 89 | if (typeof routeOptions.decompress === 'object') { 90 | // if the current endpoint has a custom compress configuration ... 91 | const mergedDecompressParams = Object.assign( 92 | {}, globalDecompressParams, processDecompressParams(routeOptions.decompress) 93 | ) 94 | 95 | buildRouteDecompress(fastify, mergedDecompressParams, routeOptions) 96 | } else if (routeOptions.decompress === false) { 97 | // don't apply any decompress settings 98 | } else { 99 | throw new Error('Unknown value for route decompress configuration') 100 | } 101 | } else if (globalDecompressParams.global) { 102 | // if the plugin is set globally (meaning that all the routes will be decompressed) 103 | // As the endpoint, does not have a custom rateLimit configuration, use the global one. 104 | buildRouteDecompress(fastify, globalDecompressParams, routeOptions) 105 | } 106 | }) 107 | 108 | next() 109 | } 110 | 111 | const defaultCompressibleTypes = /^text\/(?!event-stream)|(?:\+|\/)json(?:;|$)|(?:\+|\/)text(?:;|$)|(?:\+|\/)xml(?:;|$)|octet-stream(?:;|$)/u 112 | const recommendedDefaultBrotliOptions = { 113 | params: { 114 | // Default of 4 as 11 has a heavy impact on performance. 115 | // https://blog.cloudflare.com/this-is-brotli-from-origin#testing 116 | [zlib.constants.BROTLI_PARAM_QUALITY]: 4 117 | } 118 | } 119 | 120 | function processCompressParams (opts) { 121 | /* c8 ignore next 3 */ 122 | if (!opts) { 123 | return 124 | } 125 | 126 | const params = { 127 | global: (typeof opts.global === 'boolean') ? opts.global : true 128 | } 129 | 130 | params.removeContentLengthHeader = typeof opts.removeContentLengthHeader === 'boolean' ? opts.removeContentLengthHeader : true 131 | params.brotliOptions = params.global 132 | ? { ...recommendedDefaultBrotliOptions, ...opts.brotliOptions } 133 | : opts.brotliOptions 134 | params.zlibOptions = opts.zlibOptions 135 | params.onUnsupportedEncoding = opts.onUnsupportedEncoding 136 | params.inflateIfDeflated = opts.inflateIfDeflated === true 137 | params.threshold = typeof opts.threshold === 'number' ? opts.threshold : 1024 138 | params.compressibleTypes = opts.customTypes instanceof RegExp 139 | ? opts.customTypes.test.bind(opts.customTypes) 140 | : typeof opts.customTypes === 'function' 141 | ? opts.customTypes 142 | : defaultCompressibleTypes.test.bind(defaultCompressibleTypes) 143 | params.compressStream = { 144 | br: () => ((opts.zlib || zlib).createBrotliCompress || zlib.createBrotliCompress)(params.brotliOptions), 145 | gzip: () => ((opts.zlib || zlib).createGzip || zlib.createGzip)(params.zlibOptions), 146 | deflate: () => ((opts.zlib || zlib).createDeflate || zlib.createDeflate)(params.zlibOptions) 147 | } 148 | params.uncompressStream = { 149 | // Currently params.uncompressStream.br() is never called as we do not have any way to autodetect brotli compression in `fastify-compress` 150 | // Brotli documentation reference: [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932) 151 | br: /* c8 ignore next */ () => ((opts.zlib || zlib).createBrotliDecompress || zlib.createBrotliDecompress)(params.brotliOptions), 152 | gzip: () => ((opts.zlib || zlib).createGunzip || zlib.createGunzip)(params.zlibOptions), 153 | deflate: () => ((opts.zlib || zlib).createInflate || zlib.createInflate)(params.zlibOptions) 154 | } 155 | 156 | const supportedEncodings = ['br', 'gzip', 'deflate', 'identity'] 157 | 158 | params.encodings = Array.isArray(opts.encodings) 159 | ? supportedEncodings 160 | .filter(encoding => opts.encodings.includes(encoding)) 161 | .sort((a, b) => opts.encodings.indexOf(a) - opts.encodings.indexOf(b)) 162 | : supportedEncodings 163 | 164 | return params 165 | } 166 | 167 | function processDecompressParams (opts) { 168 | /* c8 ignore next 3 */ 169 | if (!opts) { 170 | return 171 | } 172 | 173 | const customZlib = opts.zlib || zlib 174 | 175 | const params = { 176 | global: (typeof opts.global === 'boolean') ? opts.global : true, 177 | onUnsupportedRequestEncoding: opts.onUnsupportedRequestEncoding, 178 | onInvalidRequestPayload: opts.onInvalidRequestPayload, 179 | decompressStream: { 180 | br: customZlib.createBrotliDecompress || zlib.createBrotliDecompress, 181 | gzip: customZlib.createGunzip || zlib.createGunzip, 182 | deflate: customZlib.createInflate || zlib.createInflate 183 | }, 184 | encodings: [], 185 | forceEncoding: null 186 | } 187 | 188 | const supportedEncodings = ['br', 'gzip', 'deflate', 'identity'] 189 | 190 | params.encodings = Array.isArray(opts.requestEncodings) 191 | ? supportedEncodings 192 | .filter(encoding => opts.requestEncodings.includes(encoding)) 193 | .sort((a, b) => opts.requestEncodings.indexOf(a) - opts.requestEncodings.indexOf(b)) 194 | : supportedEncodings 195 | 196 | if (opts.forceRequestEncoding) { 197 | params.forceEncoding = opts.forceRequestEncoding 198 | 199 | if (params.encodings.includes(opts.forceRequestEncoding)) { 200 | params.encodings = [opts.forceRequestEncoding] 201 | } 202 | } 203 | 204 | return params 205 | } 206 | 207 | function buildRouteCompress (_fastify, params, routeOptions, decorateOnly) { 208 | // In order to provide a compress method with the same parameter set as the route itself, 209 | // we decorate the reply at the start of the request 210 | if (Array.isArray(routeOptions.onRequest)) { 211 | routeOptions.onRequest.push(onRequest) 212 | } else if (typeof routeOptions.onRequest === 'function') { 213 | routeOptions.onRequest = [routeOptions.onRequest, onRequest] 214 | } else { 215 | routeOptions.onRequest = [onRequest] 216 | } 217 | 218 | const compressFn = compress(params) 219 | function onRequest (_req, reply, next) { 220 | reply.compress = compressFn 221 | next() 222 | } 223 | 224 | if (decorateOnly) { 225 | return 226 | } 227 | 228 | if (Array.isArray(routeOptions.onSend)) { 229 | routeOptions.onSend.push(onSend) 230 | } else if (typeof routeOptions.onSend === 'function') { 231 | routeOptions.onSend = [routeOptions.onSend, onSend] 232 | } else { 233 | routeOptions.onSend = [onSend] 234 | } 235 | 236 | function onSend (req, reply, payload, next) { 237 | if (payload == null) { 238 | return next() 239 | } 240 | const responseEncoding = reply.getHeader('Content-Encoding') 241 | if (responseEncoding && responseEncoding !== 'identity') { 242 | // response is already compressed 243 | return next() 244 | } 245 | 246 | let stream, encoding 247 | const noCompress = 248 | // don't compress on x-no-compression header 249 | (req.headers['x-no-compression'] !== undefined) || 250 | // don't compress if not one of the indicated compressible types 251 | (shouldCompress(reply.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) || 252 | // don't compress on missing or identity `accept-encoding` header 253 | ((encoding = getEncodingHeader(params.encodings, req)) == null || encoding === 'identity') 254 | 255 | if (encoding == null && params.onUnsupportedEncoding != null) { 256 | const encodingHeader = req.headers['accept-encoding'] 257 | try { 258 | const errorPayload = params.onUnsupportedEncoding(encodingHeader, reply.request, reply) 259 | return next(null, errorPayload) 260 | } catch (err) { 261 | return next(err) 262 | } 263 | } 264 | 265 | if (noCompress) { 266 | if (params.inflateIfDeflated && isStream(stream = maybeUnzip(payload))) { 267 | encoding === undefined 268 | ? reply.removeHeader('Content-Encoding') 269 | : reply.header('Content-Encoding', 'identity') 270 | pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply)) 271 | } 272 | return next(null, payload) 273 | } 274 | 275 | if (typeof payload.pipe !== 'function') { 276 | if (Buffer.byteLength(payload) < params.threshold) { 277 | return next() 278 | } 279 | payload = Readable.from(intoAsyncIterator(payload)) 280 | } 281 | 282 | setVaryHeader(reply) 283 | reply.header('Content-Encoding', encoding) 284 | if (params.removeContentLengthHeader) { 285 | reply.removeHeader('content-length') 286 | } 287 | 288 | stream = zipStream(params.compressStream, encoding) 289 | pump(payload, stream, onEnd.bind(reply)) 290 | next(null, stream) 291 | } 292 | } 293 | 294 | function buildRouteDecompress (_fastify, params, routeOptions) { 295 | // Add our decompress handler in the preParsing hook 296 | if (Array.isArray(routeOptions.preParsing)) { 297 | routeOptions.preParsing.unshift(preParsing) 298 | } else if (typeof routeOptions.preParsing === 'function') { 299 | routeOptions.preParsing = [preParsing, routeOptions.preParsing] 300 | } else { 301 | routeOptions.preParsing = [preParsing] 302 | } 303 | 304 | function preParsing (request, _reply, raw, next) { 305 | // Get the encoding from the options or from the headers 306 | let encoding = params.forceEncoding 307 | 308 | if (!encoding) { 309 | encoding = request.headers['content-encoding'] 310 | } 311 | 312 | // The request is not compressed, nothing to do here 313 | if (!encoding) { 314 | return next(null, raw) 315 | } 316 | 317 | // Check that encoding is supported 318 | if (!params.encodings.includes(encoding)) { 319 | let errorPayload 320 | 321 | if (params.onUnsupportedRequestEncoding) { 322 | try { 323 | errorPayload = params.onUnsupportedRequestEncoding(encoding, request) 324 | } catch { 325 | errorPayload = undefined 326 | } 327 | } 328 | 329 | if (!errorPayload) { 330 | errorPayload = new InvalidRequestEncodingError(encoding) 331 | } 332 | 333 | return next(errorPayload) 334 | } 335 | 336 | // No action on identity 337 | if (encoding === 'identity') { 338 | return next(null, raw) 339 | } 340 | 341 | // Prepare decompression - If there is a decompress error, prepare the error for fastify handing 342 | const decompresser = params.decompressStream[encoding]() 343 | decompresser.receivedEncodedLength = 0 344 | decompresser.on('error', onDecompressError.bind(this, request, params, encoding)) 345 | decompresser.pause() 346 | 347 | // Track length of encoded length to handle receivedEncodedLength 348 | raw.on('data', trackEncodedLength.bind(decompresser)) 349 | raw.on('end', removeEncodedLengthTracking) 350 | 351 | next(null, pump(raw, decompresser)) 352 | } 353 | } 354 | 355 | function compress (params) { 356 | return function (payload) { 357 | if (payload == null) { 358 | this.send(new Error('Internal server error')) 359 | return 360 | } 361 | 362 | let stream, encoding 363 | const noCompress = 364 | // don't compress on x-no-compression header 365 | (this.request.headers['x-no-compression'] !== undefined) || 366 | // don't compress if not one of the indicated compressible types 367 | (shouldCompress(this.getHeader('Content-Type') || 'application/json', params.compressibleTypes) === false) || 368 | // don't compress on missing or identity `accept-encoding` header 369 | ((encoding = getEncodingHeader(params.encodings, this.request)) == null || encoding === 'identity') 370 | 371 | if (encoding == null && params.onUnsupportedEncoding != null) { 372 | const encodingHeader = this.request.headers['accept-encoding'] 373 | 374 | let errorPayload 375 | try { 376 | errorPayload = params.onUnsupportedEncoding(encodingHeader, this.request, this) 377 | } catch (ex) { 378 | errorPayload = ex 379 | } 380 | return this.send(errorPayload) 381 | } 382 | 383 | if (noCompress) { 384 | if (params.inflateIfDeflated && isStream(stream = maybeUnzip(payload, this.serialize.bind(this)))) { 385 | encoding === undefined 386 | ? this.removeHeader('Content-Encoding') 387 | : this.header('Content-Encoding', 'identity') 388 | pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this)) 389 | } 390 | return this.send(payload) 391 | } 392 | 393 | if (typeof payload.pipe !== 'function') { 394 | if (!Buffer.isBuffer(payload) && typeof payload !== 'string') { 395 | payload = this.serialize(payload) 396 | } 397 | } 398 | 399 | if (typeof payload.pipe !== 'function') { 400 | if (Buffer.byteLength(payload) < params.threshold) { 401 | return this.send(payload) 402 | } 403 | payload = Readable.from(intoAsyncIterator(payload)) 404 | } 405 | 406 | setVaryHeader(this) 407 | this.header('Content-Encoding', encoding) 408 | if (params.removeContentLengthHeader) { 409 | this.removeHeader('content-length') 410 | } 411 | 412 | stream = zipStream(params.compressStream, encoding) 413 | pump(payload, stream, onEnd.bind(this)) 414 | this.send(stream) 415 | } 416 | } 417 | 418 | function setVaryHeader (reply) { 419 | if (reply.hasHeader('Vary')) { 420 | const rawHeaderValue = reply.getHeader('Vary') 421 | const headerValueArray = Array.isArray(rawHeaderValue) ? rawHeaderValue : [rawHeaderValue] 422 | if (!headerValueArray.some((h) => h.includes('accept-encoding'))) { 423 | reply.header('Vary', headerValueArray.concat('accept-encoding').join(', ')) 424 | } 425 | } else { 426 | reply.header('Vary', 'accept-encoding') 427 | } 428 | } 429 | 430 | function onEnd (err) { 431 | if (err) this.log.error(err) 432 | } 433 | 434 | function trackEncodedLength (chunk) { 435 | this.receivedEncodedLength += chunk.length 436 | } 437 | 438 | function removeEncodedLengthTracking () { 439 | this.removeListener('data', trackEncodedLength) 440 | this.removeListener('end', removeEncodedLengthTracking) 441 | } 442 | 443 | function onDecompressError (request, params, encoding, error) { 444 | this.log.debug(`compress: invalid request payload - ${error}`) 445 | 446 | let errorPayload 447 | 448 | if (params.onInvalidRequestPayload) { 449 | try { 450 | errorPayload = params.onInvalidRequestPayload(encoding, request, error) 451 | } catch { 452 | errorPayload = undefined 453 | } 454 | } 455 | 456 | if (!errorPayload) { 457 | errorPayload = new InvalidRequestCompressedPayloadError() 458 | } 459 | 460 | error.decompressError = error 461 | Object.assign(error, errorPayload) 462 | } 463 | 464 | const gzipAlias = /\*|x-gzip/gu 465 | 466 | function getEncodingHeader (encodings, request) { 467 | let header = request.headers['accept-encoding'] 468 | if (header != null) { 469 | header = header.toLowerCase() 470 | // consider the no-preference token as gzip for downstream compat 471 | // and x-gzip as an alias of gzip 472 | // ref.: [HTTP/1.1 RFC 7230 section 4.2.3](https://datatracker.ietf.org/doc/html/rfc7230#section-4.2.3) 473 | .replace(gzipAlias, 'gzip') 474 | return encodingNegotiator.negotiate(header, encodings) 475 | } else { 476 | return undefined 477 | } 478 | } 479 | 480 | function shouldCompress (type, compressibleTypes) { 481 | if (compressibleTypes(type)) return true 482 | const data = mimedb[type.split(';', 1)[0].trim().toLowerCase()] 483 | if (data === undefined) return false 484 | return data.compressible === true 485 | } 486 | 487 | function isCompressed (data) { 488 | if (isGzip(data)) return 1 489 | if (isDeflate(data)) return 2 490 | return 0 491 | } 492 | 493 | function maybeUnzip (payload, serialize) { 494 | if (isStream(payload)) return payload 495 | 496 | let buf = payload; let result = payload 497 | 498 | if (ArrayBuffer.isView(payload)) { 499 | // Cast non-Buffer DataViews into a Buffer 500 | buf = result = Buffer.from( 501 | payload.buffer, 502 | payload.byteOffset, 503 | payload.byteLength 504 | ) 505 | } else if (serialize && typeof payload !== 'string') { 506 | buf = result = serialize(payload) 507 | } 508 | 509 | // handle case where serialize doesn't return a string or Buffer 510 | if (!Buffer.isBuffer(buf)) return result 511 | if (isCompressed(buf) === 0) return result 512 | return Readable.from(intoAsyncIterator(result)) 513 | } 514 | 515 | function zipStream (deflate, encoding) { 516 | return peek({ newline: false, maxBuffer: 10 }, function (data, swap) { 517 | switch (isCompressed(data)) { 518 | case 1: return swap(null, new Minipass()) 519 | case 2: return swap(null, new Minipass()) 520 | } 521 | return swap(null, deflate[encoding]()) 522 | }) 523 | } 524 | 525 | function unzipStream (inflate, maxRecursion) { 526 | if (!(maxRecursion >= 0)) maxRecursion = 3 527 | return peek({ newline: false, maxBuffer: 10 }, function (data, swap) { 528 | // This path is never taken, when `maxRecursion` < 0 it is automatically set back to 3 529 | /* c8 ignore next */ 530 | if (maxRecursion < 0) return swap(new Error('Maximum recursion reached')) 531 | switch (isCompressed(data)) { 532 | case 1: return swap(null, pumpify(inflate.gzip(), unzipStream(inflate, maxRecursion - 1))) 533 | case 2: return swap(null, pumpify(inflate.deflate(), unzipStream(inflate, maxRecursion - 1))) 534 | } 535 | return swap(null, new Minipass()) 536 | }) 537 | } 538 | 539 | function createError (code, message, statusCode) { 540 | code = code.toUpperCase() 541 | 542 | function FastifyCompressError (a) { 543 | Error.captureStackTrace(this, FastifyCompressError) 544 | this.name = 'FastifyCompressError' 545 | this.code = code 546 | 547 | if (a) { 548 | this.message = format(message, a) 549 | } else { 550 | this.message = message 551 | } 552 | 553 | this.statusCode = statusCode 554 | } 555 | 556 | FastifyCompressError.prototype[Symbol.toStringTag] = 'Error' 557 | 558 | /* c8 ignore next 3 */ 559 | FastifyCompressError.prototype.toString = function () { 560 | return `${this.name} [${this.code}]: ${this.message}` 561 | } 562 | 563 | inherits(FastifyCompressError, Error) 564 | 565 | return FastifyCompressError 566 | } 567 | 568 | module.exports = fp(fastifyCompress, { 569 | fastify: '5.x', 570 | name: '@fastify/compress' 571 | }) 572 | module.exports.default = fastifyCompress 573 | module.exports.fastifyCompress = fastifyCompress 574 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // https://datatracker.ietf.org/doc/html/rfc1950#section-2 4 | function isDeflate (buffer) { 5 | return ( 6 | typeof buffer === 'object' && 7 | buffer !== null && 8 | buffer.length > 1 && 9 | // CM = 8 denotes the "deflate" compression method 10 | (buffer[0] & 0x0f) === 0x08 && 11 | // CINFO Values of above 7 are not allowed by RFC 1950 12 | (buffer[0] & 0x80) === 0 && 13 | // The FCHECK value must be such that CMF and FLG, when viewed as 14 | // a 16-bit unsigned integer stored in MSB order (CMF*256 + FLG), 15 | // is a multiple of 31. 16 | (((buffer[0] << 8) + buffer[1]) % 31) === 0 17 | ) 18 | } 19 | 20 | // https://datatracker.ietf.org/doc/html/rfc1952#page-6 21 | function isGzip (buffer) { 22 | return ( 23 | typeof buffer === 'object' && 24 | buffer !== null && 25 | buffer.length > 2 && 26 | // ID1 27 | buffer[0] === 0x1f && 28 | // ID2 29 | buffer[1] === 0x8b && 30 | buffer[2] === 0x08 31 | ) 32 | } 33 | 34 | function isStream (stream) { 35 | return stream !== null && typeof stream === 'object' && typeof stream.pipe === 'function' 36 | } 37 | 38 | /** 39 | * Provide a async iteratable for Readable.from 40 | */ 41 | async function * intoAsyncIterator (payload) { 42 | if (typeof payload === 'object') { 43 | if (Buffer.isBuffer(payload)) { 44 | yield payload 45 | return 46 | } 47 | 48 | if ( 49 | // ArrayBuffer 50 | payload instanceof ArrayBuffer || 51 | // NodeJS.TypedArray 52 | ArrayBuffer.isView(payload) 53 | ) { 54 | yield Buffer.from(payload) 55 | return 56 | } 57 | 58 | // Iterator 59 | if (Symbol.iterator in payload) { 60 | for (const chunk of payload) { 61 | yield chunk 62 | } 63 | return 64 | } 65 | 66 | // Async Iterator 67 | if (Symbol.asyncIterator in payload) { 68 | for await (const chunk of payload) { 69 | yield chunk 70 | } 71 | return 72 | } 73 | } 74 | 75 | // string 76 | yield payload 77 | } 78 | 79 | module.exports = { isGzip, isDeflate, isStream, intoAsyncIterator } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@fastify/compress", 3 | "version": "8.0.2", 4 | "description": "Fastify compression utils", 5 | "main": "index.js", 6 | "type": "commonjs", 7 | "types": "types/index.d.ts", 8 | "dependencies": { 9 | "@fastify/accept-negotiator": "^2.0.0", 10 | "fastify-plugin": "^5.0.0", 11 | "mime-db": "^1.52.0", 12 | "minipass": "^7.0.4", 13 | "peek-stream": "^1.1.3", 14 | "pump": "^3.0.0", 15 | "pumpify": "^2.0.1", 16 | "readable-stream": "^4.5.2" 17 | }, 18 | "devDependencies": { 19 | "@fastify/pre-commit": "^2.1.0", 20 | "@types/node": "^22.0.0", 21 | "adm-zip": "^0.5.12", 22 | "c8": "^10.1.2", 23 | "eslint": "^9.17.0", 24 | "fastify": "^5.0.0", 25 | "jsonstream": "^1.0.3", 26 | "neostandard": "^0.12.0", 27 | "tsd": "^0.31.0", 28 | "typescript": "~5.7.2" 29 | }, 30 | "scripts": { 31 | "lint": "eslint", 32 | "lint:fix": "eslint --fix", 33 | "test": "npm run test:unit && npm run test:typescript", 34 | "test:typescript": "tsd", 35 | "test:unit": "node --test", 36 | "test:coverage": "c8 node --test && c8 report --reporter=html", 37 | "test:unit:verbose": "npm run test:unit -- -Rspec" 38 | }, 39 | "keywords": [ 40 | "fastify", 41 | "compression", 42 | "deflate", 43 | "gzip", 44 | "brotli" 45 | ], 46 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 47 | "contributors": [ 48 | { 49 | "name": "Matteo Collina", 50 | "email": "hello@matteocollina.com" 51 | }, 52 | { 53 | "name": "Manuel Spigolon", 54 | "email": "behemoth89@gmail.com" 55 | }, 56 | { 57 | "name": "Aras Abbasi", 58 | "email": "aras.abbasi@gmail.com" 59 | }, 60 | { 61 | "name": "Frazer Smith", 62 | "email": "frazer.dev@icloud.com", 63 | "url": "https://github.com/fdawgs" 64 | } 65 | ], 66 | "license": "MIT", 67 | "bugs": { 68 | "url": "https://github.com/fastify/fastify-compress/issues" 69 | }, 70 | "homepage": "https://github.com/fastify/fastify-compress#readme", 71 | "funding": [ 72 | { 73 | "type": "github", 74 | "url": "https://github.com/sponsors/fastify" 75 | }, 76 | { 77 | "type": "opencollective", 78 | "url": "https://opencollective.com/fastify" 79 | } 80 | ], 81 | "repository": { 82 | "type": "git", 83 | "url": "git+https://github.com/fastify/fastify-compress.git" 84 | }, 85 | "tsd": { 86 | "directory": "test/types" 87 | }, 88 | "publishConfig": { 89 | "access": "public" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/global-compress.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { describe, test } = require('node:test') 4 | const { createReadStream, readFile, readFileSync } = require('node:fs') 5 | const { Readable, Writable, PassThrough } = require('node:stream') 6 | const zlib = require('node:zlib') 7 | const AdmZip = require('adm-zip') 8 | const JSONStream = require('jsonstream') 9 | const Fastify = require('fastify') 10 | const compressPlugin = require('../index') 11 | const { once } = require('node:events') 12 | 13 | describe('When `global` is not set, it is `true` by default :', async () => { 14 | test('it should compress Buffer data using brotli when `Accept-Encoding` request header is `br`', async (t) => { 15 | t.plan(1) 16 | 17 | const fastify = Fastify() 18 | await fastify.register(compressPlugin, { threshold: 0 }) 19 | 20 | const buf = Buffer.from('hello world') 21 | fastify.get('/', (_request, reply) => { 22 | reply.send(buf) 23 | }) 24 | 25 | const response = await fastify.inject({ 26 | url: '/', 27 | method: 'GET', 28 | headers: { 29 | 'accept-encoding': 'br' 30 | } 31 | }) 32 | const payload = zlib.brotliDecompressSync(response.rawPayload) 33 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 34 | }) 35 | 36 | test('it should compress Buffer data using deflate when `Accept-Encoding` request header is `deflate`', async (t) => { 37 | t.plan(1) 38 | 39 | const fastify = Fastify() 40 | await fastify.register(compressPlugin, { threshold: 0 }) 41 | 42 | const buf = Buffer.from('hello world') 43 | fastify.get('/', (_request, reply) => { 44 | reply.send(buf) 45 | }) 46 | 47 | const response = await fastify.inject({ 48 | url: '/', 49 | method: 'GET', 50 | headers: { 51 | 'accept-encoding': 'deflate' 52 | } 53 | }) 54 | const payload = zlib.inflateSync(response.rawPayload) 55 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 56 | }) 57 | 58 | test('it should compress Buffer data using gzip when `Accept-Encoding` request header is `gzip`', async (t) => { 59 | t.plan(1) 60 | 61 | const fastify = Fastify() 62 | await fastify.register(compressPlugin, { threshold: 0 }) 63 | 64 | const buf = Buffer.from('hello world') 65 | fastify.get('/', (_request, reply) => { 66 | reply.send(buf) 67 | }) 68 | 69 | const response = await fastify.inject({ 70 | url: '/', 71 | method: 'GET', 72 | headers: { 73 | 'accept-encoding': 'gzip' 74 | } 75 | }) 76 | const payload = zlib.gunzipSync(response.rawPayload) 77 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 78 | }) 79 | 80 | test('it should compress JSON data using brotli when `Accept-Encoding` request header is `br`', async (t) => { 81 | t.plan(1) 82 | 83 | const fastify = Fastify() 84 | await fastify.register(compressPlugin, { threshold: 0 }) 85 | 86 | const json = { hello: 'world' } 87 | 88 | fastify.get('/', (_request, reply) => { 89 | reply.send(json) 90 | }) 91 | 92 | const response = await fastify.inject({ 93 | url: '/', 94 | method: 'GET', 95 | headers: { 96 | 'accept-encoding': 'br' 97 | } 98 | }) 99 | const payload = zlib.brotliDecompressSync(response.rawPayload) 100 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 101 | }) 102 | 103 | test('it should compress JSON data using deflate when `Accept-Encoding` request header is `deflate`', async (t) => { 104 | t.plan(1) 105 | 106 | const fastify = Fastify() 107 | await fastify.register(compressPlugin, { threshold: 0 }) 108 | 109 | const json = { hello: 'world' } 110 | 111 | fastify.get('/', (_request, reply) => { 112 | reply.send(json) 113 | }) 114 | 115 | const response = await fastify.inject({ 116 | url: '/', 117 | method: 'GET', 118 | headers: { 119 | 'accept-encoding': 'deflate' 120 | } 121 | }) 122 | const payload = zlib.inflateSync(response.rawPayload) 123 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 124 | }) 125 | 126 | test('it should compress JSON data using gzip when `Accept-Encoding` request header is `gzip`', async (t) => { 127 | t.plan(1) 128 | 129 | const fastify = Fastify() 130 | await fastify.register(compressPlugin, { threshold: 0 }) 131 | 132 | const json = { hello: 'world' } 133 | fastify.get('/', (_request, reply) => { 134 | reply.send(json) 135 | }) 136 | 137 | const response = await fastify.inject({ 138 | url: '/', 139 | method: 'GET', 140 | headers: { 141 | 'accept-encoding': 'gzip' 142 | } 143 | }) 144 | const payload = zlib.gunzipSync(response.rawPayload) 145 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 146 | }) 147 | 148 | test('it should compress string data using brotli when `Accept-Encoding` request header is `br', async (t) => { 149 | t.plan(1) 150 | 151 | const fastify = Fastify() 152 | await fastify.register(compressPlugin, { threshold: 0 }) 153 | 154 | fastify.get('/', (_request, reply) => { 155 | reply 156 | .type('text/plain') 157 | .send('hello') 158 | }) 159 | 160 | const response = await fastify.inject({ 161 | url: '/', 162 | method: 'GET', 163 | headers: { 164 | 'accept-encoding': 'br' 165 | } 166 | }) 167 | const payload = zlib.brotliDecompressSync(response.rawPayload) 168 | t.assert.equal(payload.toString('utf-8'), 'hello') 169 | }) 170 | 171 | test('it should compress string data using deflate when `Accept-Encoding` request header is `deflate', async (t) => { 172 | t.plan(1) 173 | 174 | const fastify = Fastify() 175 | await fastify.register(compressPlugin, { threshold: 0 }) 176 | 177 | fastify.get('/', (_request, reply) => { 178 | reply 179 | .type('text/plain') 180 | .compress('hello') 181 | }) 182 | 183 | const response = await fastify.inject({ 184 | url: '/', 185 | method: 'GET', 186 | headers: { 187 | 'accept-encoding': 'deflate' 188 | } 189 | }) 190 | const payload = zlib.inflateSync(response.rawPayload) 191 | t.assert.equal(payload.toString('utf-8'), 'hello') 192 | }) 193 | 194 | test('it should compress string data using gzip when `Accept-Encoding` request header is `gzip', async (t) => { 195 | t.plan(1) 196 | 197 | const fastify = Fastify() 198 | await fastify.register(compressPlugin, { threshold: 0 }) 199 | 200 | fastify.get('/', (_request, reply) => { 201 | reply 202 | .type('text/plain') 203 | .compress('hello') 204 | }) 205 | 206 | const response = await fastify.inject({ 207 | url: '/', 208 | method: 'GET', 209 | headers: { 210 | 'accept-encoding': 'gzip' 211 | } 212 | }) 213 | const payload = zlib.gunzipSync(response.rawPayload) 214 | t.assert.equal(payload.toString('utf-8'), 'hello') 215 | }) 216 | }) 217 | 218 | describe('It should send compressed Stream data when `global` is `true` :', async () => { 219 | test('using brotli when `Accept-Encoding` request header is `br`', async (t) => { 220 | t.plan(3) 221 | 222 | const fastify = Fastify() 223 | await fastify.register(compressPlugin, { global: true }) 224 | 225 | fastify.get('/', (_request, reply) => { 226 | reply 227 | .type('text/plain') 228 | .compress(createReadStream('./package.json')) 229 | }) 230 | 231 | const response = await fastify.inject({ 232 | url: '/', 233 | method: 'GET', 234 | headers: { 235 | 'accept-encoding': 'br' 236 | } 237 | }) 238 | const file = readFileSync('./package.json', 'utf8') 239 | const payload = zlib.brotliDecompressSync(response.rawPayload) 240 | t.assert.equal(response.headers.vary, 'accept-encoding') 241 | t.assert.equal(response.headers['content-encoding'], 'br') 242 | t.assert.equal(payload.toString('utf-8'), file) 243 | }) 244 | 245 | test('using deflate when `Accept-Encoding` request header is `deflate`', async (t) => { 246 | t.plan(4) 247 | 248 | const fastify = Fastify() 249 | await fastify.register(compressPlugin, { global: true }) 250 | 251 | fastify.get('/', (_request, reply) => { 252 | reply 253 | .type('text/plain') 254 | .compress(createReadStream('./package.json')) 255 | }) 256 | 257 | const response = await fastify.inject({ 258 | url: '/', 259 | method: 'GET', 260 | headers: { 261 | 'accept-encoding': 'deflate' 262 | } 263 | }) 264 | const file = readFileSync('./package.json', 'utf8') 265 | const payload = zlib.inflateSync(response.rawPayload) 266 | t.assert.equal(response.headers.vary, 'accept-encoding') 267 | t.assert.equal(response.headers['content-encoding'], 'deflate') 268 | t.assert.ok(!response.headers['content-length'], 'no content length') 269 | t.assert.equal(payload.toString('utf-8'), file) 270 | }) 271 | 272 | test('using gzip when `Accept-Encoding` request header is `gzip`', async (t) => { 273 | t.plan(3) 274 | 275 | const fastify = Fastify() 276 | await fastify.register(compressPlugin, { global: true }) 277 | 278 | fastify.get('/', (_request, reply) => { 279 | reply 280 | .type('text/plain') 281 | .compress(createReadStream('./package.json')) 282 | }) 283 | 284 | const response = await fastify.inject({ 285 | url: '/', 286 | method: 'GET', 287 | headers: { 288 | 'accept-encoding': 'gzip' 289 | } 290 | }) 291 | const file = readFileSync('./package.json', 'utf8') 292 | const payload = zlib.gunzipSync(response.rawPayload) 293 | t.assert.equal(response.headers.vary, 'accept-encoding') 294 | t.assert.equal(response.headers['content-encoding'], 'gzip') 295 | t.assert.equal(payload.toString('utf-8'), file) 296 | }) 297 | }) 298 | 299 | describe('It should send compressed Buffer data when `global` is `true` :', async () => { 300 | test('using brotli when `Accept-Encoding` request header is `br`', async (t) => { 301 | t.plan(1) 302 | 303 | const fastify = Fastify() 304 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 305 | 306 | const buf = Buffer.from('hello world') 307 | fastify.get('/', (_request, reply) => { 308 | reply.compress(buf) 309 | }) 310 | 311 | const response = await fastify.inject({ 312 | url: '/', 313 | method: 'GET', 314 | headers: { 315 | 'accept-encoding': 'br' 316 | } 317 | }) 318 | const payload = zlib.brotliDecompressSync(response.rawPayload) 319 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 320 | }) 321 | 322 | test('using deflate when `Accept-Encoding` request header is `deflate`', async (t) => { 323 | t.plan(1) 324 | 325 | const fastify = Fastify() 326 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 327 | 328 | const buf = Buffer.from('hello world') 329 | fastify.get('/', (_request, reply) => { 330 | reply.compress(buf) 331 | }) 332 | 333 | const response = await fastify.inject({ 334 | url: '/', 335 | method: 'GET', 336 | headers: { 337 | 'accept-encoding': 'deflate' 338 | } 339 | }) 340 | const payload = zlib.inflateSync(response.rawPayload) 341 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 342 | }) 343 | 344 | test('using gzip when `Accept-Encoding` request header is `gzip`', async (t) => { 345 | t.plan(1) 346 | 347 | const fastify = Fastify() 348 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 349 | 350 | const buf = Buffer.from('hello world') 351 | fastify.get('/', (_request, reply) => { 352 | reply.compress(buf) 353 | }) 354 | 355 | const response = await fastify.inject({ 356 | url: '/', 357 | method: 'GET', 358 | headers: { 359 | 'accept-encoding': 'gzip' 360 | } 361 | }) 362 | const payload = zlib.gunzipSync(response.rawPayload) 363 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 364 | }) 365 | }) 366 | 367 | describe('It should send compressed JSON data when `global` is `true` :', async () => { 368 | test('using brotli when `Accept-Encoding` request header is `br`', async (t) => { 369 | t.plan(1) 370 | 371 | const fastify = Fastify() 372 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 373 | 374 | const json = { hello: 'world' } 375 | fastify.get('/', (_request, reply) => { 376 | reply.compress(json) 377 | }) 378 | 379 | const response = await fastify.inject({ 380 | url: '/', 381 | method: 'GET', 382 | headers: { 383 | 'accept-encoding': 'br' 384 | } 385 | }) 386 | const payload = zlib.brotliDecompressSync(response.rawPayload) 387 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 388 | }) 389 | 390 | test('using deflate when `Accept-Encoding` request header is `deflate`', async (t) => { 391 | t.plan(1) 392 | 393 | const fastify = Fastify() 394 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 395 | 396 | const json = { hello: 'world' } 397 | fastify.get('/', (_request, reply) => { 398 | reply.compress(json) 399 | }) 400 | 401 | const response = await fastify.inject({ 402 | url: '/', 403 | method: 'GET', 404 | headers: { 405 | 'accept-encoding': 'deflate' 406 | } 407 | }) 408 | const payload = zlib.inflateSync(response.rawPayload) 409 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 410 | }) 411 | 412 | test('using gzip when `Accept-Encoding` request header is `gzip`', async (t) => { 413 | t.plan(1) 414 | 415 | const fastify = Fastify() 416 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 417 | 418 | const json = { hello: 'world' } 419 | fastify.get('/', (_request, reply) => { 420 | reply.compress(json) 421 | }) 422 | 423 | const response = await fastify.inject({ 424 | url: '/', 425 | method: 'GET', 426 | headers: { 427 | 'accept-encoding': 'gzip' 428 | } 429 | }) 430 | const payload = zlib.gunzipSync(response.rawPayload) 431 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 432 | }) 433 | }) 434 | 435 | describe('It should fallback to the default `gzip` encoding compression :', async () => { 436 | test('when `Accept-Encoding` request header value is set to `*`', async (t) => { 437 | t.plan(3) 438 | 439 | const fastify = Fastify() 440 | await fastify.register(compressPlugin, { global: true }) 441 | 442 | fastify.get('/', (_request, reply) => { 443 | reply 444 | .type('text/plain') 445 | .compress(createReadStream('./package.json')) 446 | }) 447 | 448 | const response = await fastify.inject({ 449 | url: '/', 450 | method: 'GET', 451 | headers: { 452 | 'accept-encoding': '*' 453 | } 454 | }) 455 | const file = readFileSync('./package.json', 'utf8') 456 | const payload = zlib.gunzipSync(response.rawPayload) 457 | t.assert.equal(response.headers.vary, 'accept-encoding') 458 | t.assert.equal(response.headers['content-encoding'], 'gzip') 459 | t.assert.equal(payload.toString('utf-8'), file) 460 | }) 461 | 462 | test('when `Accept-Encoding` request header value is set to multiple `*` directives', async (t) => { 463 | t.plan(3) 464 | 465 | const fastify = Fastify() 466 | await fastify.register(compressPlugin, { global: true }) 467 | 468 | fastify.get('/', (_request, reply) => { 469 | reply 470 | .type('text/plain') 471 | .compress(createReadStream('./package.json')) 472 | }) 473 | 474 | const response = await fastify.inject({ 475 | url: '/', 476 | method: 'GET', 477 | headers: { 478 | 'accept-encoding': '*,*' 479 | } 480 | }) 481 | const file = readFileSync('./package.json', 'utf8') 482 | const payload = zlib.gunzipSync(response.rawPayload) 483 | t.assert.equal(response.headers.vary, 'accept-encoding') 484 | t.assert.equal(response.headers['content-encoding'], 'gzip') 485 | t.assert.equal(payload.toString('utf-8'), file) 486 | }) 487 | }) 488 | 489 | describe('When a custom `zlib` option is provided, it should compress data :`', async () => { 490 | test('using the custom `createBrotliCompress()` method', async (t) => { 491 | t.plan(5) 492 | 493 | let usedCustom = false 494 | const customZlib = { createBrotliCompress: () => (usedCustom = true) && zlib.createBrotliCompress() } 495 | 496 | const fastify = Fastify() 497 | await fastify.register(compressPlugin, { global: true, zlib: customZlib }) 498 | 499 | fastify.get('/', (_request, reply) => { 500 | reply 501 | .type('text/plain') 502 | .compress(createReadStream('./package.json')) 503 | }) 504 | 505 | const response = await fastify.inject({ 506 | url: '/', 507 | method: 'GET', 508 | headers: { 509 | 'accept-encoding': 'br' 510 | } 511 | }) 512 | t.assert.equal(usedCustom, true) 513 | 514 | const file = readFileSync('./package.json', 'utf8') 515 | const payload = zlib.brotliDecompressSync(response.rawPayload) 516 | t.assert.equal(response.headers.vary, 'accept-encoding') 517 | t.assert.equal(response.headers['content-encoding'], 'br') 518 | t.assert.ok(!response.headers['content-length'], 'no content length') 519 | t.assert.equal(payload.toString('utf-8'), file) 520 | }) 521 | 522 | test('using the custom `createDeflate()` method', async (t) => { 523 | t.plan(5) 524 | 525 | let usedCustom = false 526 | const customZlib = { createDeflate: () => (usedCustom = true) && zlib.createDeflate() } 527 | 528 | const fastify = Fastify() 529 | await fastify.register(compressPlugin, { global: true, zlib: customZlib }) 530 | 531 | fastify.get('/', (_request, reply) => { 532 | reply 533 | .type('text/plain') 534 | .compress(createReadStream('./package.json')) 535 | }) 536 | 537 | const response = await fastify.inject({ 538 | url: '/', 539 | method: 'GET', 540 | headers: { 541 | 'accept-encoding': 'deflate' 542 | } 543 | }) 544 | t.assert.equal(usedCustom, true) 545 | 546 | const file = readFileSync('./package.json', 'utf8') 547 | const payload = zlib.inflateSync(response.rawPayload) 548 | t.assert.equal(response.headers.vary, 'accept-encoding') 549 | t.assert.equal(response.headers['content-encoding'], 'deflate') 550 | t.assert.ok(!response.headers['content-length'], 'no content length') 551 | t.assert.equal(payload.toString('utf-8'), file) 552 | }) 553 | 554 | test('using the custom `createGzip()` method', async (t) => { 555 | t.plan(4) 556 | 557 | let usedCustom = false 558 | const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() } 559 | 560 | const fastify = Fastify() 561 | await fastify.register(compressPlugin, { global: true, zlib: customZlib }) 562 | 563 | fastify.get('/', (_request, reply) => { 564 | reply 565 | .type('text/plain') 566 | .compress(createReadStream('./package.json')) 567 | }) 568 | 569 | const response = await fastify.inject({ 570 | url: '/', 571 | method: 'GET', 572 | headers: { 573 | 'accept-encoding': 'gzip' 574 | } 575 | }) 576 | t.assert.equal(usedCustom, true) 577 | 578 | const file = readFileSync('./package.json', 'utf8') 579 | const payload = zlib.gunzipSync(response.rawPayload) 580 | t.assert.equal(response.headers.vary, 'accept-encoding') 581 | t.assert.equal(response.headers['content-encoding'], 'gzip') 582 | t.assert.equal(payload.toString('utf-8'), file) 583 | }) 584 | }) 585 | 586 | describe('When a malformed custom `zlib` option is provided, it should compress data :', async () => { 587 | test('using the fallback default Node.js core `zlib.createBrotliCompress()` method', async (t) => { 588 | t.plan(1) 589 | 590 | const fastify = Fastify() 591 | await fastify.register(compressPlugin, { 592 | global: true, 593 | threshold: 0, 594 | zlib: true // will trigger a fallback on the default zlib.createBrotliCompress 595 | }) 596 | 597 | fastify.get('/', (_request, reply) => { 598 | reply 599 | .type('text/plain') 600 | .compress('hello') 601 | }) 602 | 603 | const response = await fastify.inject({ 604 | url: '/', 605 | method: 'GET', 606 | headers: { 607 | 'accept-encoding': 'br' 608 | } 609 | }) 610 | const payload = zlib.brotliDecompressSync(response.rawPayload) 611 | t.assert.equal(payload.toString('utf-8'), 'hello') 612 | }) 613 | 614 | test('using the fallback default Node.js core `zlib.createDeflate()` method', async (t) => { 615 | t.plan(1) 616 | 617 | const fastify = Fastify() 618 | await fastify.register(compressPlugin, { 619 | global: true, 620 | threshold: 0, 621 | zlib: true // will trigger a fallback on the default zlib.createDeflate 622 | }) 623 | 624 | fastify.get('/', (_request, reply) => { 625 | reply 626 | .type('text/plain') 627 | .compress('hello') 628 | }) 629 | 630 | const response = await fastify.inject({ 631 | url: '/', 632 | method: 'GET', 633 | headers: { 634 | 'accept-encoding': 'deflate' 635 | } 636 | }) 637 | const payload = zlib.inflateSync(response.rawPayload) 638 | t.assert.equal(payload.toString('utf-8'), 'hello') 639 | }) 640 | 641 | test('using the fallback default Node.js core `zlib.createGzip()` method', async (t) => { 642 | t.plan(1) 643 | 644 | const fastify = Fastify() 645 | await fastify.register(compressPlugin, { 646 | global: true, 647 | threshold: 0, 648 | zlib: true // will trigger a fallback on the default zlib.createGzip 649 | }) 650 | 651 | fastify.get('/', (_request, reply) => { 652 | reply 653 | .type('text/plain') 654 | .compress('hello') 655 | }) 656 | 657 | const response = await fastify.inject({ 658 | url: '/', 659 | method: 'GET', 660 | headers: { 661 | 'accept-encoding': 'gzip' 662 | } 663 | }) 664 | const payload = zlib.gunzipSync(response.rawPayload) 665 | t.assert.equal(payload.toString('utf-8'), 'hello') 666 | }) 667 | }) 668 | 669 | describe('When `inflateIfDeflated` is `true` and `X-No-Compression` request header is `true` :', async () => { 670 | test('it should uncompress payloads using the deflate algorithm', async (t) => { 671 | t.plan(4) 672 | 673 | const fastify = Fastify() 674 | await fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) 675 | 676 | const json = { hello: 'world' } 677 | fastify.get('/', (_request, reply) => { 678 | reply.send(zlib.deflateSync(JSON.stringify(json))) 679 | }) 680 | 681 | const response = await fastify.inject({ 682 | url: '/', 683 | method: 'GET', 684 | headers: { 685 | 'x-no-compression': true 686 | } 687 | }) 688 | t.assert.equal(response.statusCode, 200) 689 | t.assert.ok(!response.headers.vary) 690 | t.assert.ok(!response.headers['content-encoding']) 691 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 692 | }) 693 | 694 | test('it should uncompress payloads using the gzip algorithm', async (t) => { 695 | t.plan(4) 696 | 697 | const fastify = Fastify() 698 | await fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) 699 | 700 | const json = { hello: 'world' } 701 | fastify.get('/', (_request, reply) => { 702 | reply.send(zlib.gzipSync(JSON.stringify(json))) 703 | }) 704 | 705 | const response = await fastify.inject({ 706 | url: '/', 707 | method: 'GET', 708 | headers: { 709 | 'x-no-compression': true 710 | } 711 | }) 712 | t.assert.equal(response.statusCode, 200) 713 | t.assert.ok(!response.headers.vary) 714 | t.assert.ok(!response.headers['content-encoding']) 715 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 716 | }) 717 | }) 718 | 719 | test('it should not uncompress payloads using the zip algorithm', async (t) => { 720 | t.plan(5) 721 | 722 | const fastify = Fastify() 723 | await fastify.register(compressPlugin, { threshold: 0, inflateIfDeflated: true }) 724 | 725 | const json = { hello: 'world' } 726 | const zip = new AdmZip() 727 | zip.addFile('file.zip', Buffer.from(JSON.stringify(json), 'utf-8')) 728 | const fileBuffer = zip.toBuffer() 729 | 730 | fastify.get('/', (_request, reply) => { 731 | reply.compress(fileBuffer) 732 | }) 733 | 734 | const response = await fastify.inject({ 735 | url: '/', 736 | method: 'GET', 737 | headers: { 738 | 'x-no-compression': true 739 | } 740 | }) 741 | t.assert.equal(response.statusCode, 200) 742 | t.assert.ok(!response.headers.vary) 743 | t.assert.ok(!response.headers['content-encoding']) 744 | t.assert.deepEqual(response.rawPayload, fileBuffer) 745 | t.assert.equal(response.payload, fileBuffer.toString('utf-8')) 746 | }) 747 | 748 | describe('It should not compress :', async () => { 749 | describe('Using `reply.compress()` :', async () => { 750 | test('when payload length is smaller than the `threshold` defined value', async (t) => { 751 | t.plan(4) 752 | 753 | const fastify = Fastify() 754 | await fastify.register(compressPlugin, { threshold: 128 }) 755 | 756 | fastify.get('/', (_request, reply) => { 757 | reply 758 | .type('text/plain') 759 | .compress('a message') 760 | }) 761 | 762 | const response = await fastify.inject({ 763 | url: '/', 764 | method: 'GET', 765 | headers: { 766 | 'accept-encoding': 'deflate' 767 | } 768 | }) 769 | t.assert.equal(response.statusCode, 200) 770 | t.assert.ok(!response.headers.vary) 771 | t.assert.ok(!response.headers['content-encoding']) 772 | t.assert.equal(response.payload, 'a message') 773 | }) 774 | 775 | test('when `customTypes` is set and does not match `Content-Type` reply header or `mime-db`', async (t) => { 776 | t.plan(3) 777 | 778 | const fastify = Fastify() 779 | await fastify.register(compressPlugin, { customTypes: /x-user-header$/u }) 780 | 781 | fastify.get('/', (_request, reply) => { 782 | reply 783 | .type('application/x-other-type') 784 | .compress(createReadStream('./package.json')) 785 | }) 786 | 787 | const response = await fastify.inject({ 788 | url: '/', 789 | method: 'GET', 790 | headers: { 791 | 'accept-encoding': 'gzip' 792 | } 793 | }) 794 | t.assert.ok(!response.headers.vary, 'accept-encoding') 795 | t.assert.ok(!response.headers['content-encoding']) 796 | t.assert.equal(response.statusCode, 200) 797 | }) 798 | 799 | test('when `customTypes` is a function and returns false on the provided `Content-Type` reply header`', async (t) => { 800 | t.plan(3) 801 | 802 | const fastify = Fastify() 803 | await fastify.register(compressPlugin, { customTypes: value => value === 'application/x-user-header' }) 804 | 805 | fastify.get('/', (_request, reply) => { 806 | reply 807 | .type('application/x-other-type') 808 | .compress(createReadStream('./package.json')) 809 | }) 810 | 811 | const response = await fastify.inject({ 812 | url: '/', 813 | method: 'GET', 814 | headers: { 815 | 'accept-encoding': 'gzip' 816 | } 817 | }) 818 | t.assert.ok(!response.headers.vary) 819 | t.assert.ok(!response.headers['content-encoding']) 820 | t.assert.equal(response.statusCode, 200) 821 | }) 822 | 823 | test('when `X-No-Compression` request header is `true`', async (t) => { 824 | t.plan(4) 825 | 826 | const fastify = Fastify() 827 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 828 | 829 | const json = { hello: 'world' } 830 | fastify.get('/', (_request, reply) => { 831 | reply.compress(json) 832 | }) 833 | 834 | const response = await fastify.inject({ 835 | url: '/', 836 | method: 'GET', 837 | headers: { 838 | 'x-no-compression': true 839 | } 840 | }) 841 | t.assert.equal(response.statusCode, 200) 842 | t.assert.ok(!response.headers.vary) 843 | t.assert.ok(!response.headers['content-encoding']) 844 | t.assert.deepEqual(JSON.parse(response.payload), json) 845 | }) 846 | 847 | test('when `Content-Type` reply header is not set and the content is not detected as a compressible type', async (t) => { 848 | t.plan(3) 849 | 850 | const fastify = Fastify() 851 | await fastify.register(compressPlugin, { threshold: 0 }) 852 | 853 | fastify.addHook('onSend', async (_request, response) => { 854 | response.header('Content-Type', undefined) 855 | }) 856 | 857 | const json = { hello: 'world' } 858 | fastify.get('/', (_request, reply) => { 859 | // The auto-dectection will fallback as an 'application/json' type 860 | reply.compress(json) 861 | }) 862 | 863 | const response = await fastify.inject({ 864 | url: '/', 865 | method: 'GET', 866 | headers: { 867 | accept: 'application/json', 868 | 'accept-encoding': 'identity' 869 | } 870 | }) 871 | t.assert.ok(!response.headers.vary) 872 | t.assert.ok(!response.headers['content-encoding']) 873 | t.assert.equal(response.payload, JSON.stringify(json)) 874 | }) 875 | 876 | test('when `Content-Type` reply header is a mime type with undefined compressible values', async (t) => { 877 | t.plan(4) 878 | 879 | const fastify = Fastify() 880 | await fastify.register(compressPlugin, { threshold: 0 }) 881 | 882 | fastify.get('/', (_request, reply) => { 883 | reply 884 | .type('image/webp') 885 | .compress('hello') 886 | }) 887 | 888 | const response = await fastify.inject({ 889 | url: '/', 890 | method: 'GET', 891 | headers: { 892 | 'accept-encoding': 'gzip, deflate, br' 893 | } 894 | }) 895 | t.assert.equal(response.statusCode, 200) 896 | t.assert.ok(!response.headers.vary) 897 | t.assert.ok(!response.headers['content-encoding']) 898 | t.assert.equal(response.payload, 'hello') 899 | }) 900 | 901 | test('when `Content-Type` reply header value is `text/event-stream`', async (t) => { 902 | t.plan(4) 903 | 904 | const fastify = Fastify() 905 | await fastify.register(compressPlugin, { threshold: 0 }) 906 | 907 | fastify.get('/', (_req, reply) => { 908 | const stream = new PassThrough() 909 | 910 | reply 911 | .type('text/event-stream') 912 | .compress(stream) 913 | 914 | stream.write('event: open\n\n') 915 | stream.write('event: change\ndata: schema\n\n') 916 | stream.end() 917 | }) 918 | 919 | const response = await fastify.inject({ 920 | url: '/', 921 | method: 'GET' 922 | }) 923 | t.assert.equal(response.statusCode, 200) 924 | t.assert.ok(!response.headers.vary) 925 | t.assert.ok(!response.headers['content-encoding']) 926 | t.assert.deepEqual(response.payload, 'event: open\n\nevent: change\ndata: schema\n\n') 927 | }) 928 | 929 | test('when `Content-Type` reply header value is an invalid type', async (t) => { 930 | t.plan(4) 931 | 932 | const fastify = Fastify() 933 | await fastify.register(compressPlugin, { threshold: 0 }) 934 | 935 | fastify.get('/', (_request, reply) => { 936 | reply 937 | .type('something/invalid') 938 | .compress('a message') 939 | }) 940 | 941 | const response = await fastify.inject({ 942 | url: '/', 943 | method: 'GET', 944 | headers: { 945 | 'accept-encoding': 'deflate' 946 | } 947 | }) 948 | t.assert.equal(response.statusCode, 200) 949 | t.assert.ok(!response.headers.vary) 950 | t.assert.ok(!response.headers['content-encoding']) 951 | t.assert.equal(response.payload, 'a message') 952 | }) 953 | 954 | test('when `Accept-Encoding` request header is missing', async (t) => { 955 | t.plan(3) 956 | 957 | const fastify = Fastify() 958 | await fastify.register(compressPlugin, { global: true }) 959 | 960 | fastify.get('/', (_request, reply) => { 961 | reply 962 | .type('text/plain') 963 | .compress(createReadStream('./package.json')) 964 | }) 965 | 966 | const response = await fastify.inject({ 967 | url: '/', 968 | method: 'GET' 969 | }) 970 | t.assert.equal(response.statusCode, 200) 971 | t.assert.ok(!response.headers.vary) 972 | t.assert.ok(!response.headers['content-encoding']) 973 | }) 974 | 975 | test('when `Accept-Encoding` request header is set to `identity`', async (t) => { 976 | t.plan(3) 977 | 978 | const fastify = Fastify() 979 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 980 | 981 | fastify.get('/', (_request, reply) => { 982 | reply.compress({ hello: 'world' }) 983 | }) 984 | 985 | const response = await fastify.inject({ 986 | url: '/', 987 | method: 'GET', 988 | headers: { 989 | 'accept-encoding': 'identity' 990 | } 991 | }) 992 | const payload = JSON.parse(response.payload) 993 | t.assert.ok(!response.headers.vary) 994 | t.assert.ok(!response.headers['content-encoding']) 995 | t.assert.deepEqual({ hello: 'world' }, payload) 996 | }) 997 | 998 | test('when `Accept-Encoding` request header value is not supported', async (t) => { 999 | t.plan(3) 1000 | 1001 | const fastify = Fastify() 1002 | await fastify.register(compressPlugin, { global: true }) 1003 | 1004 | fastify.get('/', (_request, reply) => { 1005 | reply 1006 | .type('text/plain') 1007 | .compress(createReadStream('./package.json')) 1008 | }) 1009 | 1010 | const response = await fastify.inject({ 1011 | url: '/', 1012 | method: 'GET', 1013 | headers: { 1014 | 'accept-encoding': 'invalid' 1015 | } 1016 | }) 1017 | const file = readFileSync('./package.json', 'utf8') 1018 | t.assert.equal(response.statusCode, 200) 1019 | t.assert.ok(!response.headers.vary) 1020 | t.assert.equal(response.payload, file) 1021 | }) 1022 | 1023 | test('when `Accept-Encoding` request header value is not supported (with quality value)', async (t) => { 1024 | t.plan(3) 1025 | 1026 | const fastify = Fastify() 1027 | await fastify.register(compressPlugin, { global: true }) 1028 | 1029 | fastify.get('/', (_request, reply) => { 1030 | reply 1031 | .type('text/plain') 1032 | .compress(createReadStream('./package.json')) 1033 | }) 1034 | 1035 | const response = await fastify.inject({ 1036 | url: '/', 1037 | method: 'GET', 1038 | headers: { 1039 | 'accept-encoding': 'lzma;q=1.0' 1040 | } 1041 | }) 1042 | const file = readFileSync('./package.json', 'utf8') 1043 | t.assert.equal(response.statusCode, 200) 1044 | t.assert.ok(!response.headers.vary) 1045 | t.assert.equal(response.payload, file) 1046 | }) 1047 | 1048 | test('when `Accept-Encoding` request header is set to `identity and `inflateIfDeflated` is `true``', async (t) => { 1049 | t.plan(3) 1050 | 1051 | const fastify = Fastify() 1052 | await fastify.register(compressPlugin, { global: true, inflateIfDeflated: true, threshold: 0 }) 1053 | 1054 | fastify.get('/', (_request, reply) => { 1055 | reply.compress({ hello: 'world' }) 1056 | }) 1057 | 1058 | const response = await fastify.inject({ 1059 | url: '/', 1060 | method: 'GET', 1061 | headers: { 1062 | 'accept-encoding': 'identity' 1063 | } 1064 | }) 1065 | const payload = JSON.parse(response.payload) 1066 | t.assert.ok(!response.headers.vary) 1067 | t.assert.ok(!response.headers['content-encoding']) 1068 | t.assert.deepEqual({ hello: 'world' }, payload) 1069 | }) 1070 | }) 1071 | 1072 | describe('Using `onSend` hook :', async () => { 1073 | test('when there is no payload', async (t) => { 1074 | t.plan(4) 1075 | 1076 | const fastify = Fastify() 1077 | await fastify.register(compressPlugin, { threshold: 0 }) 1078 | 1079 | fastify.get('/', (_request, reply) => { 1080 | reply.send(undefined) 1081 | }) 1082 | 1083 | const response = await fastify.inject({ 1084 | url: '/', 1085 | method: 'GET', 1086 | headers: { 1087 | 'accept-encoding': 'gzip' 1088 | } 1089 | }) 1090 | t.assert.equal(response.statusCode, 200) 1091 | t.assert.ok(!response.headers.vary) 1092 | t.assert.ok(!response.headers['content-encoding']) 1093 | t.assert.equal(response.payload, '') 1094 | }) 1095 | 1096 | test('when payload length is smaller than the `threshold` defined value', async (t) => { 1097 | t.plan(4) 1098 | 1099 | const fastify = Fastify() 1100 | await fastify.register(compressPlugin, { threshold: 128 }) 1101 | 1102 | fastify.get('/', (_request, reply) => { 1103 | reply 1104 | .header('Content-Type', 'text/plain') 1105 | .send('a message') 1106 | }) 1107 | 1108 | const response = await fastify.inject({ 1109 | url: '/', 1110 | method: 'GET', 1111 | headers: { 1112 | 'accept-encoding': 'deflate' 1113 | } 1114 | }) 1115 | t.assert.equal(response.statusCode, 200) 1116 | t.assert.ok(!response.headers.vary) 1117 | t.assert.ok(!response.headers['content-encoding']) 1118 | t.assert.equal(response.payload, 'a message') 1119 | }) 1120 | 1121 | test('when `customTypes` is set and does not match `Content-Type` reply header or `mime-db`', async (t) => { 1122 | t.plan(3) 1123 | 1124 | const fastify = Fastify() 1125 | await fastify.register(compressPlugin, { customTypes: /x-user-header$/u }) 1126 | 1127 | fastify.get('/', (_request, reply) => { 1128 | reply 1129 | .type('application/x-other-type') 1130 | .send(createReadStream('./package.json')) 1131 | }) 1132 | 1133 | const response = await fastify.inject({ 1134 | url: '/', 1135 | method: 'GET', 1136 | headers: { 1137 | 'accept-encoding': 'gzip' 1138 | } 1139 | }) 1140 | t.assert.ok(!response.headers.vary) 1141 | t.assert.ok(!response.headers['content-encoding']) 1142 | t.assert.equal(response.statusCode, 200) 1143 | }) 1144 | 1145 | test('when `X-No-Compression` request header is `true`', async (t) => { 1146 | t.plan(4) 1147 | 1148 | const fastify = Fastify() 1149 | await fastify.register(compressPlugin, { threshold: 0 }) 1150 | 1151 | const json = { hello: 'world' } 1152 | fastify.get('/', (_request, reply) => { 1153 | reply.send(json) 1154 | }) 1155 | 1156 | const response = await fastify.inject({ 1157 | url: '/', 1158 | method: 'GET', 1159 | headers: { 1160 | 'x-no-compression': true 1161 | } 1162 | }) 1163 | t.assert.equal(response.statusCode, 200) 1164 | t.assert.ok(!response.headers.vary) 1165 | t.assert.ok(!response.headers['content-encoding']) 1166 | t.assert.deepEqual(JSON.parse(response.payload), json) 1167 | }) 1168 | 1169 | test('when `Content-Type` reply header is not set and the content is not detected as a compressible type', async (t) => { 1170 | t.plan(3) 1171 | 1172 | const fastify = Fastify() 1173 | await fastify.register(compressPlugin, { threshold: 0 }) 1174 | 1175 | fastify.addHook('onSend', async (_request, response) => { 1176 | response.header('Content-Type', undefined) 1177 | }) 1178 | 1179 | const json = { hello: 'world' } 1180 | fastify.get('/', (_request, reply) => { 1181 | // The auto-dectection will fallback as an 'application/json' type 1182 | reply.send(json) 1183 | }) 1184 | 1185 | const response = await fastify.inject({ 1186 | url: '/', 1187 | method: 'GET', 1188 | headers: { 1189 | accept: 'application/json', 1190 | 'accept-encoding': 'identity' 1191 | } 1192 | }) 1193 | t.assert.ok(!response.headers.vary) 1194 | t.assert.ok(!response.headers['content-encoding']) 1195 | t.assert.equal(response.payload, JSON.stringify(json)) 1196 | }) 1197 | 1198 | test('when `Content-Type` reply header is a mime type with undefined compressible values', async (t) => { 1199 | t.plan(4) 1200 | 1201 | const fastify = Fastify() 1202 | await fastify.register(compressPlugin, { threshold: 0 }) 1203 | 1204 | fastify.get('/', (_request, reply) => { 1205 | reply 1206 | .type('image/webp') 1207 | .send('hello') 1208 | }) 1209 | 1210 | const response = await fastify.inject({ 1211 | url: '/', 1212 | method: 'GET', 1213 | headers: { 1214 | 'accept-encoding': 'gzip, deflate, br' 1215 | } 1216 | }) 1217 | t.assert.equal(response.statusCode, 200) 1218 | t.assert.ok(!response.headers.vary) 1219 | t.assert.ok(!response.headers['content-encoding']) 1220 | t.assert.equal(response.payload, 'hello') 1221 | }) 1222 | 1223 | test('when `Content-Type` reply header value is `text/event-stream`', async (t) => { 1224 | t.plan(4) 1225 | 1226 | const fastify = Fastify() 1227 | await fastify.register(compressPlugin, { threshold: 0 }) 1228 | 1229 | fastify.get('/', (_req, reply) => { 1230 | const stream = new PassThrough() 1231 | 1232 | reply 1233 | .header('Content-Type', 'text/event-stream') 1234 | .send(stream) 1235 | 1236 | stream.write('event: open\n\n') 1237 | stream.write('event: change\ndata: schema\n\n') 1238 | stream.end() 1239 | }) 1240 | 1241 | const response = await fastify.inject({ 1242 | url: '/', 1243 | method: 'GET' 1244 | }) 1245 | t.assert.equal(response.statusCode, 200) 1246 | t.assert.ok(!response.headers.vary) 1247 | t.assert.ok(!response.headers['content-encoding']) 1248 | t.assert.deepEqual(response.payload, 'event: open\n\nevent: change\ndata: schema\n\n') 1249 | }) 1250 | 1251 | test('when `Content-Type` reply header value is an invalid type', async (t) => { 1252 | t.plan(4) 1253 | 1254 | const fastify = Fastify() 1255 | await fastify.register(compressPlugin, { threshold: 0 }) 1256 | 1257 | fastify.get('/', (_request, reply) => { 1258 | reply 1259 | .header('Content-Type', 'something/invalid') 1260 | .send('a message') 1261 | }) 1262 | 1263 | const response = await fastify.inject({ 1264 | url: '/', 1265 | method: 'GET', 1266 | headers: { 1267 | 'accept-encoding': 'deflate' 1268 | } 1269 | }) 1270 | t.assert.equal(response.statusCode, 200) 1271 | t.assert.ok(!response.headers.vary) 1272 | t.assert.ok(!response.headers['content-encoding']) 1273 | t.assert.equal(response.payload, 'a message') 1274 | }) 1275 | 1276 | test('when `Accept-Encoding` request header is missing', async (t) => { 1277 | t.plan(3) 1278 | 1279 | const fastify = Fastify() 1280 | await fastify.register(compressPlugin, { global: true }) 1281 | 1282 | fastify.get('/', (_request, reply) => { 1283 | reply 1284 | .header('Content-Type', 'text/plain') 1285 | .send(createReadStream('./package.json')) 1286 | }) 1287 | 1288 | const response = await fastify.inject({ 1289 | url: '/', 1290 | method: 'GET' 1291 | }) 1292 | t.assert.equal(response.statusCode, 200) 1293 | t.assert.ok(!response.headers.vary) 1294 | t.assert.ok(!response.headers['content-encoding']) 1295 | }) 1296 | 1297 | test('when `Accept-Encoding` request header is set to `identity`', async (t) => { 1298 | t.plan(3) 1299 | 1300 | const fastify = Fastify() 1301 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 1302 | 1303 | fastify.get('/', (_request, reply) => { 1304 | reply.send({ hello: 'world' }) 1305 | }) 1306 | 1307 | const response = await fastify.inject({ 1308 | url: '/', 1309 | method: 'GET', 1310 | headers: { 1311 | 'accept-encoding': 'identity' 1312 | } 1313 | }) 1314 | const payload = JSON.parse(response.payload) 1315 | t.assert.ok(!response.headers.vary) 1316 | t.assert.ok(!response.headers['content-encoding']) 1317 | t.assert.deepEqual({ hello: 'world' }, payload) 1318 | }) 1319 | 1320 | test('when `Accept-Encoding` request header value is not supported', async (t) => { 1321 | t.plan(2) 1322 | 1323 | const fastify = Fastify() 1324 | await fastify.register(compressPlugin, { global: true }) 1325 | 1326 | fastify.get('/', (_request, reply) => { 1327 | reply 1328 | .header('Content-Type', 'text/plain') 1329 | .send('something') 1330 | }) 1331 | 1332 | const response = await fastify.inject({ 1333 | url: '/', 1334 | method: 'GET', 1335 | headers: { 1336 | 'accept-encoding': 'invalid' 1337 | } 1338 | }) 1339 | t.assert.equal(response.statusCode, 200) 1340 | t.assert.equal(response.payload, 'something') 1341 | }) 1342 | 1343 | test('when `Accept-Encoding` request header value is not supported (with quality value)', async (t) => { 1344 | t.plan(3) 1345 | 1346 | const fastify = Fastify() 1347 | await fastify.register(compressPlugin, { global: true }) 1348 | 1349 | fastify.get('/', (_request, reply) => { 1350 | reply 1351 | .header('Content-Type', 'text/plain') 1352 | .send(createReadStream('./package.json')) 1353 | }) 1354 | 1355 | const response = await fastify.inject({ 1356 | url: '/', 1357 | method: 'GET', 1358 | headers: { 1359 | 'accept-encoding': 'lzma;q=1.0' 1360 | } 1361 | }) 1362 | const file = readFileSync('./package.json', 'utf8') 1363 | t.assert.equal(response.statusCode, 200) 1364 | t.assert.ok(!response.headers.vary) 1365 | t.assert.equal(response.payload, file) 1366 | }) 1367 | 1368 | test('when `Accept-Encoding` request header is set to `identity and `inflateIfDeflated` is `true``', async (t) => { 1369 | t.plan(3) 1370 | 1371 | const fastify = Fastify() 1372 | await fastify.register(compressPlugin, { global: true, inflateIfDeflated: true, threshold: 0 }) 1373 | 1374 | fastify.get('/', (_request, reply) => { 1375 | reply.send({ hello: 'world' }) 1376 | }) 1377 | 1378 | const response = await fastify.inject({ 1379 | url: '/', 1380 | method: 'GET', 1381 | headers: { 1382 | 'accept-encoding': 'identity' 1383 | } 1384 | }) 1385 | const payload = JSON.parse(response.payload) 1386 | t.assert.ok(!response.headers.vary) 1387 | t.assert.ok(!response.headers['content-encoding']) 1388 | t.assert.deepEqual({ hello: 'world' }, payload) 1389 | }) 1390 | }) 1391 | }) 1392 | 1393 | describe('It should not double-compress :', async () => { 1394 | test('when using `reply.compress()` to send an already deflated Stream', async (t) => { 1395 | t.plan(3) 1396 | 1397 | const fastify = Fastify() 1398 | await fastify.register(compressPlugin, { global: true }) 1399 | 1400 | fastify.get('/', (_request, reply) => { 1401 | reply 1402 | .type('text/plain') 1403 | .compress( 1404 | createReadStream('./package.json').pipe(zlib.createDeflate()) 1405 | ) 1406 | }) 1407 | 1408 | const response = await fastify.inject({ 1409 | url: '/', 1410 | method: 'GET', 1411 | headers: { 1412 | 'accept-encoding': 'deflate' 1413 | } 1414 | }) 1415 | const file = readFileSync('./package.json', 'utf8') 1416 | const payload = zlib.inflateSync(response.rawPayload) 1417 | t.assert.equal(response.headers.vary, 'accept-encoding') 1418 | t.assert.equal(response.headers['content-encoding'], 'deflate') 1419 | t.assert.equal(payload.toString('utf-8'), file) 1420 | }) 1421 | 1422 | test('when using `reply.compress()` to send an already gzipped Stream', async (t) => { 1423 | t.plan(3) 1424 | 1425 | const fastify = Fastify() 1426 | await fastify.register(compressPlugin, { global: true }) 1427 | 1428 | fastify.get('/', (_request, reply) => { 1429 | reply 1430 | .type('text/plain') 1431 | .compress( 1432 | createReadStream('./package.json').pipe(zlib.createGzip()) 1433 | ) 1434 | }) 1435 | 1436 | const response = await fastify.inject({ 1437 | url: '/', 1438 | method: 'GET', 1439 | headers: { 1440 | 'accept-encoding': 'gzip' 1441 | } 1442 | }) 1443 | const file = readFileSync('./package.json', 'utf8') 1444 | const payload = zlib.gunzipSync(response.rawPayload) 1445 | t.assert.equal(response.headers.vary, 'accept-encoding') 1446 | t.assert.equal(response.headers['content-encoding'], 'gzip') 1447 | t.assert.equal(payload.toString('utf-8'), file) 1448 | }) 1449 | 1450 | test('when using `onSend` hook to send an already brotli compressed Stream', async (t) => { 1451 | t.plan(4) 1452 | 1453 | const fastify = Fastify() 1454 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 1455 | 1456 | const file = readFileSync('./package.json', 'utf8') 1457 | fastify.get('/', (_request, reply) => { 1458 | const payload = zlib.brotliCompressSync(file) 1459 | 1460 | reply 1461 | .type('application/json') 1462 | .header('content-encoding', 'br') 1463 | .header('content-length', payload.length) 1464 | .send(payload) 1465 | }) 1466 | 1467 | const response = await fastify.inject({ 1468 | url: '/', 1469 | method: 'GET', 1470 | headers: { 1471 | 'accept-encoding': 'br' 1472 | } 1473 | }) 1474 | const payload = zlib.brotliDecompressSync(response.rawPayload) 1475 | t.assert.ok(!response.headers.vary) 1476 | t.assert.equal(response.headers['content-encoding'], 'br') 1477 | t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString()) 1478 | t.assert.equal(payload.toString('utf-8'), file) 1479 | }) 1480 | 1481 | test('when using `onSend` hook to send an already deflated Stream', async (t) => { 1482 | t.plan(4) 1483 | 1484 | const fastify = Fastify() 1485 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 1486 | 1487 | const file = readFileSync('./package.json', 'utf8') 1488 | fastify.get('/', (_request, reply) => { 1489 | const payload = zlib.deflateSync(file) 1490 | 1491 | reply 1492 | .type('application/json') 1493 | .header('content-encoding', 'deflate') 1494 | .header('content-length', payload.length) 1495 | .send(payload) 1496 | }) 1497 | 1498 | const response = await fastify.inject({ 1499 | url: '/', 1500 | method: 'GET', 1501 | headers: { 1502 | 'accept-encoding': 'deflate' 1503 | } 1504 | }) 1505 | const payload = zlib.inflateSync(response.rawPayload) 1506 | t.assert.ok(!response.headers.vary) 1507 | t.assert.equal(response.headers['content-encoding'], 'deflate') 1508 | t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString()) 1509 | t.assert.equal(payload.toString('utf-8'), file) 1510 | }) 1511 | 1512 | test('when using `onSend` hook to send an already gzipped Stream', async (t) => { 1513 | t.plan(4) 1514 | 1515 | const fastify = Fastify() 1516 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 1517 | 1518 | const file = readFileSync('./package.json', 'utf8') 1519 | fastify.get('/', (_request, reply) => { 1520 | const payload = zlib.gzipSync(file) 1521 | 1522 | reply 1523 | .type('application/json') 1524 | .header('content-encoding', 'gzip') 1525 | .header('content-length', payload.length) 1526 | .send(payload) 1527 | }) 1528 | 1529 | const response = await fastify.inject({ 1530 | url: '/', 1531 | method: 'GET', 1532 | headers: { 1533 | 'accept-encoding': 'gzip' 1534 | } 1535 | }) 1536 | const payload = zlib.gunzipSync(response.rawPayload) 1537 | t.assert.ok(!response.headers.vary) 1538 | t.assert.equal(response.headers['content-encoding'], 'gzip') 1539 | t.assert.equal(response.headers['content-length'], response.rawPayload.length.toString()) 1540 | t.assert.equal(payload.toString('utf-8'), file) 1541 | }) 1542 | }) 1543 | 1544 | describe('It should not compress Stream data and add a `Content-Encoding` reply header :', async () => { 1545 | describe('Using `onSend` hook if `Accept-Encoding` request header value is `identity`', async () => { 1546 | test('when `inflateIfDeflated` is `true` and `encodings` is not set', async (t) => { 1547 | t.plan(4) 1548 | 1549 | const fastify = Fastify() 1550 | await fastify.register(compressPlugin, { global: true, inflateIfDeflated: true }) 1551 | 1552 | fastify.get('/', (_request, reply) => { 1553 | reply 1554 | .header('Content-Type', 'application/octet-stream') 1555 | .send(createReadStream('./package.json')) 1556 | }) 1557 | 1558 | const response = await fastify.inject({ 1559 | url: '/', 1560 | method: 'GET', 1561 | headers: { 1562 | 'accept-encoding': 'identity' 1563 | } 1564 | }) 1565 | const file = readFileSync('./package.json', 'utf8') 1566 | t.assert.equal(response.statusCode, 200) 1567 | t.assert.ok(!response.headers.vary) 1568 | t.assert.equal(response.headers['content-encoding'], 'identity') 1569 | t.assert.equal(file, response.payload) 1570 | }) 1571 | 1572 | test('when `inflateIfDeflated` is `true` and `encodings` is set', async (t) => { 1573 | t.plan(4) 1574 | 1575 | const fastify = Fastify() 1576 | await fastify.register(compressPlugin, { 1577 | global: true, 1578 | inflateIfDeflated: true, 1579 | encodings: ['deflate', 'gzip'] 1580 | }) 1581 | 1582 | fastify.get('/', (_request, reply) => { 1583 | reply.send(createReadStream('./package.json')) 1584 | }) 1585 | 1586 | const response = await fastify.inject({ 1587 | url: '/', 1588 | method: 'GET', 1589 | headers: { 1590 | accept: 'application/json', 1591 | 'accept-encoding': 'identity' 1592 | } 1593 | }) 1594 | const file = readFileSync('./package.json', 'utf-8') 1595 | t.assert.equal(response.statusCode, 200) 1596 | t.assert.ok(!response.headers.vary) 1597 | t.assert.equal(response.headers['content-encoding'], 'identity') 1598 | t.assert.deepEqual(response.payload, file) 1599 | }) 1600 | }) 1601 | 1602 | describe('Using `reply.compress()` if `Accept-Encoding` request header value is `identity`', async () => { 1603 | test('when `inflateIfDeflated` is `true` and `encodings` is not set', async (t) => { 1604 | t.plan(4) 1605 | 1606 | const fastify = Fastify() 1607 | await fastify.register(compressPlugin, { global: true, inflateIfDeflated: true }) 1608 | 1609 | fastify.get('/', (_request, reply) => { 1610 | reply 1611 | .type('application/octet-stream') 1612 | .compress(createReadStream('./package.json')) 1613 | }) 1614 | 1615 | const response = await fastify.inject({ 1616 | url: '/', 1617 | method: 'GET', 1618 | headers: { 1619 | 'accept-encoding': 'identity' 1620 | } 1621 | }) 1622 | const file = readFileSync('./package.json', 'utf8') 1623 | t.assert.equal(response.statusCode, 200) 1624 | t.assert.ok(!response.headers.vary) 1625 | t.assert.equal(response.headers['content-encoding'], 'identity') 1626 | t.assert.equal(file, response.payload) 1627 | }) 1628 | 1629 | test('when `inflateIfDeflated` is `true` and `encodings` is set', async (t) => { 1630 | t.plan(4) 1631 | 1632 | const fastify = Fastify() 1633 | await fastify.register(compressPlugin, { 1634 | global: true, 1635 | inflateIfDeflated: true, 1636 | encodings: ['deflate', 'gzip'] 1637 | }) 1638 | 1639 | fastify.get('/', (_request, reply) => { 1640 | reply.compress(createReadStream('./package.json')) 1641 | }) 1642 | 1643 | const response = await fastify.inject({ 1644 | url: '/', 1645 | method: 'GET', 1646 | headers: { 1647 | accept: 'application/json', 1648 | 'accept-encoding': 'identity' 1649 | } 1650 | }) 1651 | const file = readFileSync('./package.json', 'utf-8') 1652 | t.assert.equal(response.statusCode, 200) 1653 | t.assert.ok(!response.headers.vary) 1654 | t.assert.equal(response.headers['content-encoding'], 'identity') 1655 | t.assert.deepEqual(response.payload, file) 1656 | }) 1657 | }) 1658 | }) 1659 | 1660 | test('It should return a serialized payload when `inflateIfDeflated` is `true` and `X-No-Compression` request header is `true`', async (t) => { 1661 | t.plan(8) 1662 | 1663 | const fastify = Fastify() 1664 | await fastify.register(compressPlugin, { 1665 | global: true, 1666 | inflateIfDeflated: true, 1667 | threshold: 0 1668 | }) 1669 | 1670 | const json = { hello: 'world' } 1671 | const compressedBufferPayload = zlib.brotliCompressSync(Buffer.from(json.toString())) 1672 | 1673 | fastify.get('/one', (_request, reply) => { 1674 | reply.send(json) 1675 | }) 1676 | 1677 | fastify.get('/two', (_request, reply) => { 1678 | reply.send(compressedBufferPayload) 1679 | }) 1680 | 1681 | const one = await fastify.inject({ 1682 | url: '/one', 1683 | method: 'GET', 1684 | headers: { 1685 | 'x-no-compression': true 1686 | } 1687 | }) 1688 | t.assert.equal(one.statusCode, 200) 1689 | t.assert.ok(!one.headers.vary) 1690 | t.assert.ok(!one.headers['content-encoding']) 1691 | t.assert.deepEqual(JSON.parse(one.payload), json) 1692 | 1693 | const two = await fastify.inject({ 1694 | url: '/two', 1695 | method: 'GET', 1696 | headers: { 1697 | 'x-no-compression': true 1698 | } 1699 | }) 1700 | t.assert.equal(two.statusCode, 200) 1701 | t.assert.ok(!two.headers.vary) 1702 | t.assert.ok(!two.headers['content-encoding']) 1703 | t.assert.equal(two.payload, compressedBufferPayload.toString()) 1704 | }) 1705 | 1706 | test('It should close the stream', async (t) => { 1707 | t.plan(3) 1708 | 1709 | const fastify = Fastify() 1710 | await fastify.register(compressPlugin, { global: true }) 1711 | 1712 | const stream = createReadStream('./package.json') 1713 | const closed = once(stream, 'close') 1714 | 1715 | fastify.get('/', (_request, reply) => { 1716 | stream.on('close', () => t.assert.ok('stream closed')) 1717 | 1718 | reply 1719 | .type('text/plain') 1720 | .compress(stream) 1721 | }) 1722 | 1723 | const response = await fastify.inject({ 1724 | url: '/', 1725 | method: 'GET' 1726 | }) 1727 | 1728 | const file = readFileSync('./package.json', 'utf8') 1729 | t.assert.equal(response.statusCode, 200) 1730 | t.assert.equal(file, response.payload) 1731 | await closed 1732 | }) 1733 | 1734 | test('It should log an existing error with stream onEnd handler', async (t) => { 1735 | t.plan(1) 1736 | 1737 | let actual = null 1738 | const logger = new Writable({ 1739 | write (chunk, _encoding, callback) { 1740 | actual = JSON.parse(chunk.toString()) 1741 | callback() 1742 | } 1743 | }) 1744 | 1745 | const fastify = Fastify({ 1746 | global: true, 1747 | logger: { 1748 | level: 'error', 1749 | stream: logger 1750 | } 1751 | }) 1752 | await fastify.register(compressPlugin) 1753 | 1754 | const expect = new Error('something wrong') 1755 | 1756 | fastify.get('/', (_request, reply) => { 1757 | const stream = new Readable({ 1758 | read () { 1759 | this.destroy(expect) 1760 | } 1761 | }) 1762 | 1763 | reply 1764 | .type('text/plain') 1765 | .compress(stream) 1766 | }) 1767 | 1768 | await fastify.inject({ 1769 | url: '/', 1770 | method: 'GET', 1771 | headers: { 1772 | 'accept-encoding': 'gzip' 1773 | } 1774 | }) 1775 | t.assert.equal(actual.msg, expect.message) 1776 | }) 1777 | 1778 | describe('It should support stream1 :', async () => { 1779 | test('when using `reply.compress()`', async (t) => { 1780 | t.plan(3) 1781 | 1782 | const fastify = Fastify() 1783 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 1784 | 1785 | fastify.get('/', (_request, reply) => { 1786 | const stream = JSONStream.stringify() 1787 | 1788 | reply 1789 | .type('text/plain') 1790 | .compress(stream) 1791 | 1792 | stream.write({ hello: 'world' }) 1793 | stream.end({ a: 42 }) 1794 | }) 1795 | 1796 | const response = await fastify.inject({ 1797 | url: '/', 1798 | method: 'GET', 1799 | headers: { 1800 | 'accept-encoding': 'gzip' 1801 | } 1802 | }) 1803 | const payload = zlib.gunzipSync(response.rawPayload) 1804 | t.assert.equal(response.headers.vary, 'accept-encoding') 1805 | t.assert.equal(response.headers['content-encoding'], 'gzip') 1806 | t.assert.deepEqual(JSON.parse(payload.toString()), [{ hello: 'world' }, { a: 42 }]) 1807 | }) 1808 | 1809 | test('when using `onSend` hook', async (t) => { 1810 | t.plan(3) 1811 | 1812 | const fastify = Fastify() 1813 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 1814 | 1815 | fastify.get('/', (_request, reply) => { 1816 | const stream = JSONStream.stringify() 1817 | 1818 | reply 1819 | .type('text/plain') 1820 | .send(stream) 1821 | 1822 | stream.write({ hello: 'world' }) 1823 | stream.end({ a: 42 }) 1824 | }) 1825 | 1826 | const response = await fastify.inject({ 1827 | url: '/', 1828 | method: 'GET', 1829 | headers: { 1830 | 'accept-encoding': 'gzip' 1831 | } 1832 | }) 1833 | const payload = zlib.gunzipSync(response.rawPayload) 1834 | t.assert.equal(response.headers.vary, 'accept-encoding') 1835 | t.assert.equal(response.headers['content-encoding'], 'gzip') 1836 | t.assert.deepEqual(JSON.parse(payload.toString()), [{ hello: 'world' }, { a: 42 }]) 1837 | }) 1838 | }) 1839 | 1840 | describe('It should remove `Content-Length` header :', async () => { 1841 | test('using `reply.compress()`', async (t) => { 1842 | t.plan(4) 1843 | 1844 | const fastify = Fastify() 1845 | await fastify.register(compressPlugin, { global: true }) 1846 | 1847 | fastify.get('/', (_request, reply) => { 1848 | readFile('./package.json', 'utf8', (err, data) => { 1849 | if (err) { 1850 | return reply.send(err) 1851 | } 1852 | 1853 | reply 1854 | .type('text/plain') 1855 | .header('content-length', '' + data.length) 1856 | .compress(data) 1857 | }) 1858 | }) 1859 | 1860 | const response = await fastify.inject({ 1861 | url: '/', 1862 | method: 'GET', 1863 | headers: { 1864 | 'accept-encoding': 'deflate' 1865 | } 1866 | }) 1867 | const file = readFileSync('./package.json', 'utf8') 1868 | const payload = zlib.inflateSync(response.rawPayload) 1869 | t.assert.equal(response.headers.vary, 'accept-encoding') 1870 | t.assert.equal(response.headers['content-encoding'], 'deflate') 1871 | t.assert.ok(!response.headers['content-length'], 'no content length') 1872 | t.assert.equal(payload.toString('utf-8'), file) 1873 | }) 1874 | 1875 | test('using `reply.compress()` on a Stream when `inflateIfDeflated` is `true` and `X-No-Compression` request header is `true`', async (t) => { 1876 | t.plan(4) 1877 | 1878 | const fastify = Fastify() 1879 | await fastify.register(compressPlugin, { global: true, inflateIfDeflated: true }) 1880 | 1881 | fastify.get('/', (_request, reply) => { 1882 | reply 1883 | .type('application/octet-stream') 1884 | .compress(createReadStream('./package.json')) 1885 | }) 1886 | 1887 | const response = await fastify.inject({ 1888 | url: '/', 1889 | method: 'GET', 1890 | headers: { 1891 | 'x-no-compression': true 1892 | } 1893 | }) 1894 | const file = readFileSync('./package.json', 'utf8') 1895 | t.assert.equal(response.statusCode, 200) 1896 | t.assert.ok(!response.headers.vary) 1897 | t.assert.ok(!response.headers['content-length'], 'no content length') 1898 | t.assert.equal(file, response.payload) 1899 | }) 1900 | 1901 | test('using `onSend` hook', async (t) => { 1902 | t.plan(4) 1903 | 1904 | const fastify = Fastify() 1905 | await fastify.register(compressPlugin, { global: true }) 1906 | 1907 | fastify.get('/', (_request, reply) => { 1908 | readFile('./package.json', 'utf8', (err, data) => { 1909 | if (err) { 1910 | return reply.send(err) 1911 | } 1912 | 1913 | reply 1914 | .type('text/plain') 1915 | .header('content-length', '' + data.length) 1916 | .send(data) 1917 | }) 1918 | }) 1919 | 1920 | const response = await fastify.inject({ 1921 | url: '/', 1922 | method: 'GET', 1923 | headers: { 1924 | 'accept-encoding': 'deflate' 1925 | } 1926 | }) 1927 | const file = readFileSync('./package.json', 'utf8') 1928 | const payload = zlib.inflateSync(response.rawPayload) 1929 | t.assert.equal(response.headers.vary, 'accept-encoding') 1930 | t.assert.equal(response.headers['content-encoding'], 'deflate') 1931 | t.assert.ok(!response.headers['content-length'], 'no content length') 1932 | t.assert.equal(payload.toString('utf-8'), file) 1933 | }) 1934 | 1935 | test('using `onSend` hook on a Stream When `inflateIfDeflated` is `true` and `X-No-Compression` request header is `true`', async (t) => { 1936 | t.plan(4) 1937 | 1938 | const fastify = Fastify() 1939 | await fastify.register(compressPlugin, { global: true, inflateIfDeflated: true }) 1940 | 1941 | fastify.get('/', (_request, reply) => { 1942 | reply 1943 | .header('Content-Type', 'application/octet-stream') 1944 | .send(createReadStream('./package.json')) 1945 | }) 1946 | 1947 | const response = await fastify.inject({ 1948 | url: '/', 1949 | method: 'GET', 1950 | headers: { 1951 | 'x-no-compression': true 1952 | } 1953 | }) 1954 | const file = readFileSync('./package.json', 'utf8') 1955 | t.assert.equal(response.statusCode, 200) 1956 | t.assert.ok(!response.headers.vary) 1957 | t.assert.ok(!response.headers['content-length'], 'no content length') 1958 | t.assert.equal(file, response.payload) 1959 | }) 1960 | }) 1961 | 1962 | describe('When `removeContentLengthHeader` is `false`, it should not remove `Content-Length` header :', async () => { 1963 | test('using `reply.compress()`', async (t) => { 1964 | t.plan(4) 1965 | 1966 | const fastify = Fastify() 1967 | await fastify.register(compressPlugin, { global: true, removeContentLengthHeader: false }) 1968 | 1969 | fastify.get('/', (_request, reply) => { 1970 | readFile('./package.json', 'utf8', (err, data) => { 1971 | if (err) { 1972 | return reply.send(err) 1973 | } 1974 | 1975 | reply 1976 | .type('text/plain') 1977 | .header('content-length', '' + data.length) 1978 | .compress(data) 1979 | }) 1980 | }) 1981 | 1982 | const response = await fastify.inject({ 1983 | url: '/', 1984 | method: 'GET', 1985 | headers: { 1986 | 'accept-encoding': 'deflate' 1987 | } 1988 | }) 1989 | const file = readFileSync('./package.json', 'utf8') 1990 | const payload = zlib.inflateSync(response.rawPayload) 1991 | t.assert.equal(response.headers.vary, 'accept-encoding') 1992 | t.assert.equal(response.headers['content-encoding'], 'deflate') 1993 | t.assert.equal(response.headers['content-length'], payload.length.toString()) 1994 | t.assert.equal(payload.toString('utf-8'), file) 1995 | }) 1996 | 1997 | test('using `onSend` hook', async (t) => { 1998 | t.plan(4) 1999 | 2000 | const fastify = Fastify() 2001 | await fastify.register(compressPlugin, { global: true, removeContentLengthHeader: false }) 2002 | 2003 | fastify.get('/', (_request, reply) => { 2004 | readFile('./package.json', 'utf8', (err, data) => { 2005 | if (err) { 2006 | return reply.send(err) 2007 | } 2008 | 2009 | reply 2010 | .type('text/plain') 2011 | .header('content-length', '' + data.length) 2012 | .send(data) 2013 | }) 2014 | }) 2015 | 2016 | const response = await fastify.inject({ 2017 | url: '/', 2018 | method: 'GET', 2019 | headers: { 2020 | 'accept-encoding': 'deflate' 2021 | } 2022 | }) 2023 | const file = readFileSync('./package.json', 'utf8') 2024 | const payload = zlib.inflateSync(response.rawPayload) 2025 | t.assert.equal(response.headers.vary, 'accept-encoding') 2026 | t.assert.equal(response.headers['content-encoding'], 'deflate') 2027 | t.assert.equal(response.headers['content-length'], payload.length.toString()) 2028 | t.assert.equal(payload.toString('utf-8'), file) 2029 | }) 2030 | }) 2031 | 2032 | describe('It should add hooks correctly: ', async () => { 2033 | test('`onRequest` hooks', async (t) => { 2034 | t.plan(14) 2035 | 2036 | const fastify = Fastify() 2037 | 2038 | fastify.addHook('onRequest', async (_request, reply) => { 2039 | reply.header('x-fastify-global-test', 'ok') 2040 | }) 2041 | 2042 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 2043 | 2044 | fastify.get('/one', { 2045 | onRequest: [ 2046 | async (_request, reply) => { reply.header('x-fastify-test-one', 'ok') } 2047 | ] 2048 | }, (_request, reply) => { 2049 | reply 2050 | .type('text/plain') 2051 | .compress(createReadStream('./package.json')) 2052 | }) 2053 | 2054 | fastify.get('/two', { 2055 | onRequest: async (_request, reply) => { reply.header('x-fastify-test-two', 'ok') } 2056 | }, (_request, reply) => { 2057 | reply 2058 | .type('text/plain') 2059 | .compress(createReadStream('./package.json')) 2060 | }) 2061 | 2062 | fastify.get('/three', { onRequest: null }, (_request, reply) => { 2063 | reply 2064 | .type('text/plain') 2065 | .compress(createReadStream('./package.json')) 2066 | }) 2067 | 2068 | const file = readFileSync('./package.json', 'utf8') 2069 | await fastify.inject({ 2070 | url: '/one', 2071 | method: 'GET', 2072 | headers: { 2073 | 'accept-encoding': 'deflate' 2074 | } 2075 | }).then((response) => { 2076 | const payload = zlib.inflateSync(response.rawPayload) 2077 | t.assert.equal(response.statusCode, 200) 2078 | t.assert.ok(!response.headers['content-length'], 'no content length') 2079 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2080 | t.assert.equal(response.headers['x-fastify-test-one'], 'ok') 2081 | t.assert.equal(payload.toString('utf-8'), file) 2082 | }).catch((err) => { 2083 | t.error(err) 2084 | }) 2085 | 2086 | await fastify.inject({ 2087 | url: '/two', 2088 | method: 'GET', 2089 | headers: { 2090 | 'accept-encoding': 'deflate' 2091 | } 2092 | }).then((response) => { 2093 | const payload = zlib.inflateSync(response.rawPayload) 2094 | t.assert.equal(response.statusCode, 200) 2095 | t.assert.ok(!response.headers['content-length'], 'no content length') 2096 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2097 | t.assert.equal(response.headers['x-fastify-test-two'], 'ok') 2098 | t.assert.equal(payload.toString('utf-8'), file) 2099 | }).catch((err) => { 2100 | t.error(err) 2101 | }) 2102 | 2103 | await fastify.inject({ 2104 | url: '/three', 2105 | method: 'GET', 2106 | headers: { 2107 | 'accept-encoding': 'deflate' 2108 | } 2109 | }).then((response) => { 2110 | const payload = zlib.inflateSync(response.rawPayload) 2111 | t.assert.equal(response.statusCode, 200) 2112 | t.assert.ok(!response.headers['content-length'], 'no content length') 2113 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2114 | t.assert.equal(payload.toString('utf-8'), file) 2115 | }).catch((err) => { 2116 | t.error(err) 2117 | }) 2118 | }) 2119 | 2120 | test('`onSend` hooks', async (t) => { 2121 | t.plan(14) 2122 | 2123 | const fastify = Fastify() 2124 | 2125 | fastify.addHook('onSend', async (_request, reply) => { 2126 | reply.header('x-fastify-global-test', 'ok') 2127 | }) 2128 | 2129 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 2130 | 2131 | fastify.get('/one', { 2132 | onSend: [ 2133 | async (_request, reply) => { reply.header('x-fastify-test-one', 'ok') } 2134 | ] 2135 | }, (_request, reply) => { 2136 | reply 2137 | .type('text/plain') 2138 | .compress(createReadStream('./package.json')) 2139 | }) 2140 | 2141 | fastify.get('/two', { 2142 | onSend: async (_request, reply) => { reply.header('x-fastify-test-two', 'ok') } 2143 | }, (_request, reply) => { 2144 | reply 2145 | .type('text/plain') 2146 | .compress(createReadStream('./package.json')) 2147 | }) 2148 | 2149 | fastify.get('/three', { onSend: null }, (_request, reply) => { 2150 | reply 2151 | .type('text/plain') 2152 | .compress(createReadStream('./package.json')) 2153 | }) 2154 | 2155 | const file = readFileSync('./package.json', 'utf8') 2156 | await fastify.inject({ 2157 | url: '/one', 2158 | method: 'GET', 2159 | headers: { 2160 | 'accept-encoding': 'deflate' 2161 | } 2162 | }).then((response) => { 2163 | const payload = zlib.inflateSync(response.rawPayload) 2164 | t.assert.equal(response.statusCode, 200) 2165 | t.assert.ok(!response.headers['content-length'], 'no content length') 2166 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2167 | t.assert.equal(response.headers['x-fastify-test-one'], 'ok') 2168 | t.assert.equal(payload.toString('utf-8'), file) 2169 | }).catch((err) => { 2170 | t.error(err) 2171 | }) 2172 | 2173 | await fastify.inject({ 2174 | url: '/two', 2175 | method: 'GET', 2176 | headers: { 2177 | 'accept-encoding': 'deflate' 2178 | } 2179 | }).then((response) => { 2180 | const payload = zlib.inflateSync(response.rawPayload) 2181 | t.assert.equal(response.statusCode, 200) 2182 | t.assert.ok(!response.headers['content-length'], 'no content length') 2183 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2184 | t.assert.equal(response.headers['x-fastify-test-two'], 'ok') 2185 | t.assert.equal(payload.toString('utf-8'), file) 2186 | }).catch((err) => { 2187 | t.error(err) 2188 | }) 2189 | 2190 | await fastify.inject({ 2191 | url: '/three', 2192 | method: 'GET', 2193 | headers: { 2194 | 'accept-encoding': 'deflate' 2195 | } 2196 | }).then((response) => { 2197 | const payload = zlib.inflateSync(response.rawPayload) 2198 | t.assert.equal(response.statusCode, 200) 2199 | t.assert.ok(!response.headers['content-length'], 'no content length') 2200 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2201 | t.assert.equal(payload.toString('utf-8'), file) 2202 | }).catch((err) => { 2203 | t.error(err) 2204 | }) 2205 | }) 2206 | 2207 | test('`preParsing` hooks', async (t) => { 2208 | t.plan(14) 2209 | 2210 | const fastify = Fastify() 2211 | 2212 | fastify.addHook('preParsing', async (_request, reply, payload) => { 2213 | reply.header('x-fastify-global-test', 'ok') 2214 | return payload 2215 | }) 2216 | 2217 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 2218 | 2219 | fastify.get('/one', { 2220 | preParsing: [ 2221 | async (_request, reply, payload) => { 2222 | reply.header('x-fastify-test-one', 'ok') 2223 | return payload 2224 | } 2225 | ] 2226 | }, (_request, reply) => { 2227 | reply 2228 | .type('text/plain') 2229 | .compress(createReadStream('./package.json')) 2230 | }) 2231 | 2232 | fastify.get('/two', { 2233 | preParsing: async (_request, reply, payload) => { 2234 | reply.header('x-fastify-test-two', 'ok') 2235 | return payload 2236 | } 2237 | }, (_request, reply) => { 2238 | reply 2239 | .type('text/plain') 2240 | .compress(createReadStream('./package.json')) 2241 | }) 2242 | 2243 | fastify.get('/three', { preParsing: null }, (_request, reply) => { 2244 | reply 2245 | .type('text/plain') 2246 | .compress(createReadStream('./package.json')) 2247 | }) 2248 | 2249 | const file = readFileSync('./package.json', 'utf8') 2250 | await fastify.inject({ 2251 | url: '/one', 2252 | method: 'GET', 2253 | headers: { 2254 | 'accept-encoding': 'deflate' 2255 | } 2256 | }).then((response) => { 2257 | const payload = zlib.inflateSync(response.rawPayload) 2258 | t.assert.equal(response.statusCode, 200) 2259 | t.assert.ok(!response.headers['content-length'], 'no content length') 2260 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2261 | t.assert.equal(response.headers['x-fastify-test-one'], 'ok') 2262 | t.assert.equal(payload.toString('utf-8'), file) 2263 | }).catch((err) => { 2264 | t.error(err) 2265 | }) 2266 | 2267 | await fastify.inject({ 2268 | url: '/two', 2269 | method: 'GET', 2270 | headers: { 2271 | 'accept-encoding': 'deflate' 2272 | } 2273 | }).then((response) => { 2274 | const payload = zlib.inflateSync(response.rawPayload) 2275 | t.assert.equal(response.statusCode, 200) 2276 | t.assert.ok(!response.headers['content-length'], 'no content length') 2277 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2278 | t.assert.equal(response.headers['x-fastify-test-two'], 'ok') 2279 | t.assert.equal(payload.toString('utf-8'), file) 2280 | }).catch((err) => { 2281 | t.error(err) 2282 | }) 2283 | 2284 | await fastify.inject({ 2285 | url: '/three', 2286 | method: 'GET', 2287 | headers: { 2288 | 'accept-encoding': 'deflate' 2289 | } 2290 | }).then((response) => { 2291 | const payload = zlib.inflateSync(response.rawPayload) 2292 | t.assert.equal(response.statusCode, 200) 2293 | t.assert.ok(!response.headers['content-length'], 'no content length') 2294 | t.assert.equal(response.headers['x-fastify-global-test'], 'ok') 2295 | t.assert.equal(payload.toString('utf-8'), file) 2296 | }).catch((err) => { 2297 | t.error(err) 2298 | }) 2299 | }) 2300 | }) 2301 | 2302 | describe('When `Accept-Encoding` request header values are not supported and `onUnsupportedEncoding` is defined :', async () => { 2303 | test('it should call the defined `onUnsupportedEncoding()` method', async (t) => { 2304 | t.plan(2) 2305 | 2306 | const fastify = Fastify() 2307 | await fastify.register(compressPlugin, { 2308 | global: true, 2309 | onUnsupportedEncoding: (encoding, _request, reply) => { 2310 | reply.code(406) 2311 | return JSON.stringify({ hello: encoding }) 2312 | } 2313 | }) 2314 | 2315 | fastify.get('/', (_request, reply) => { 2316 | reply 2317 | .header('Content-Type', 'text/plain') 2318 | .send(createReadStream('./package.json')) 2319 | }) 2320 | 2321 | const response = await fastify.inject({ 2322 | url: '/', 2323 | method: 'GET', 2324 | headers: { 2325 | 'accept-encoding': 'hello' 2326 | } 2327 | }) 2328 | t.assert.equal(response.statusCode, 406) 2329 | t.assert.deepEqual(JSON.parse(response.payload), { hello: 'hello' }) 2330 | }) 2331 | 2332 | test('it should call the defined `onUnsupportedEncoding()` method and throw an error', async (t) => { 2333 | t.plan(2) 2334 | 2335 | const fastify = Fastify() 2336 | await fastify.register(compressPlugin, { 2337 | global: true, 2338 | onUnsupportedEncoding: (_encoding, _request, reply) => { 2339 | reply.code(406) 2340 | throw new Error('testing error') 2341 | } 2342 | }) 2343 | 2344 | fastify.get('/', (_request, reply) => { 2345 | reply 2346 | .header('Content-Type', 'text/plain') 2347 | .send(createReadStream('./package.json')) 2348 | }) 2349 | 2350 | const response = await fastify.inject({ 2351 | url: '/', 2352 | method: 'GET', 2353 | headers: { 2354 | 'accept-encoding': 'hello' 2355 | } 2356 | }) 2357 | t.assert.equal(response.statusCode, 406) 2358 | t.assert.deepEqual(JSON.parse(response.payload), { 2359 | error: 'Not Acceptable', 2360 | message: 'testing error', 2361 | statusCode: 406 2362 | }) 2363 | }) 2364 | }) 2365 | 2366 | describe('`Accept-Encoding` request header values :', async () => { 2367 | test('can contain white space', async (t) => { 2368 | t.plan(3) 2369 | 2370 | const fastify = Fastify() 2371 | await fastify.register(compressPlugin, { threshold: 0 }) 2372 | 2373 | const json = { hello: 'world' } 2374 | 2375 | fastify.get('/', (_request, reply) => { 2376 | reply.send(json) 2377 | }) 2378 | 2379 | const response = await fastify.inject({ 2380 | url: '/', 2381 | method: 'GET', 2382 | headers: { 2383 | 'accept-encoding': 'hello, gzip' 2384 | } 2385 | }) 2386 | const payload = zlib.gunzipSync(response.rawPayload) 2387 | t.assert.equal(response.headers.vary, 'accept-encoding') 2388 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2389 | t.assert.equal(payload.toString('utf-8'), JSON.stringify(json)) 2390 | }) 2391 | 2392 | test('can contain mixed uppercase and lowercase characters (e.g.: compressing a Stream)', async (t) => { 2393 | t.plan(3) 2394 | 2395 | const fastify = Fastify() 2396 | await fastify.register(compressPlugin, { global: true }) 2397 | 2398 | fastify.get('/', (_request, reply) => { 2399 | reply 2400 | .type('text/plain') 2401 | .compress(createReadStream('./package.json')) 2402 | }) 2403 | 2404 | const response = await fastify.inject({ 2405 | url: '/', 2406 | method: 'GET', 2407 | headers: { 2408 | 'accept-encoding': 'GZiP' 2409 | } 2410 | }) 2411 | const file = readFileSync('./package.json', 'utf8') 2412 | const payload = zlib.gunzipSync(response.rawPayload) 2413 | t.assert.equal(response.headers.vary, 'accept-encoding') 2414 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2415 | t.assert.equal(payload.toString('utf-8'), file) 2416 | }) 2417 | 2418 | test('can contain mixed uppercase and lowercase characters (e.g.: compressing a Buffer)', async (t) => { 2419 | t.plan(1) 2420 | 2421 | const fastify = Fastify() 2422 | await fastify.register(compressPlugin, { global: true, threshold: 0 }) 2423 | 2424 | const buf = Buffer.from('hello world') 2425 | fastify.get('/', (_request, reply) => { 2426 | reply.compress(buf) 2427 | }) 2428 | 2429 | const response = await fastify.inject({ 2430 | url: '/', 2431 | method: 'GET', 2432 | headers: { 2433 | 'accept-encoding': 'GzIp' 2434 | } 2435 | }) 2436 | const payload = zlib.gunzipSync(response.rawPayload) 2437 | t.assert.equal(payload.toString('utf-8'), buf.toString()) 2438 | }) 2439 | 2440 | test('should support `gzip` alias value `x-gzip`', async (t) => { 2441 | t.plan(3) 2442 | 2443 | const fastify = Fastify() 2444 | await fastify.register(compressPlugin, { global: true }) 2445 | 2446 | fastify.get('/', (_request, reply) => { 2447 | reply 2448 | .type('text/plain') 2449 | .compress( 2450 | createReadStream('./package.json') 2451 | ) 2452 | }) 2453 | 2454 | const response = await fastify.inject({ 2455 | url: '/', 2456 | method: 'GET', 2457 | headers: { 2458 | 'accept-encoding': 'x-gzip' 2459 | } 2460 | }) 2461 | const file = readFileSync('./package.json', 'utf8') 2462 | const payload = zlib.gunzipSync(response.rawPayload) 2463 | t.assert.equal(response.headers.vary, 'accept-encoding') 2464 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2465 | t.assert.equal(payload.toString('utf-8'), file) 2466 | }) 2467 | 2468 | test('should support quality syntax', async (t) => { 2469 | t.plan(3) 2470 | 2471 | const fastify = Fastify() 2472 | await fastify.register(compressPlugin, { global: true }) 2473 | 2474 | fastify.get('/', (_request, reply) => { 2475 | reply 2476 | .type('text/plain') 2477 | .compress( 2478 | createReadStream('./package.json').pipe(zlib.createDeflate()) 2479 | ) 2480 | }) 2481 | 2482 | const response = await fastify.inject({ 2483 | url: '/', 2484 | method: 'GET', 2485 | headers: { 2486 | 'accept-encoding': 'gzip;q=0.5,deflate;q=0.6,identity;q=0.3' 2487 | } 2488 | }) 2489 | const file = readFileSync('./package.json', 'utf8') 2490 | const payload = zlib.inflateSync(response.rawPayload) 2491 | t.assert.equal(response.headers.vary, 'accept-encoding') 2492 | t.assert.equal(response.headers['content-encoding'], 'deflate') 2493 | t.assert.equal(payload.toString('utf-8'), file) 2494 | }) 2495 | }) 2496 | 2497 | test('It should compress data if `customTypes` is set and matches `Content-Type` reply header value', async (t) => { 2498 | t.plan(3) 2499 | const fastify = Fastify() 2500 | await fastify.register(compressPlugin, { customTypes: /x-user-header$/u }) 2501 | 2502 | fastify.get('/', (_request, reply) => { 2503 | reply 2504 | .type('application/x-user-header') 2505 | .send(createReadStream('./package.json')) 2506 | }) 2507 | 2508 | const response = await fastify.inject({ 2509 | url: '/', 2510 | method: 'GET', 2511 | headers: { 2512 | 'accept-encoding': 'gzip' 2513 | } 2514 | }) 2515 | const file = readFileSync('./package.json', 'utf8') 2516 | const payload = zlib.gunzipSync(response.rawPayload) 2517 | t.assert.equal(response.headers.vary, 'accept-encoding') 2518 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2519 | t.assert.equal(payload.toString('utf-8'), file) 2520 | }) 2521 | 2522 | test('It should compress data if `customTypes` is a function and returns true on the provided `Content-Type` reply header value', async (t) => { 2523 | t.plan(3) 2524 | const fastify = Fastify() 2525 | await fastify.register(compressPlugin, { customTypes: value => value === 'application/x-user-header' }) 2526 | 2527 | fastify.get('/', (_request, reply) => { 2528 | reply 2529 | .type('application/x-user-header') 2530 | .send(createReadStream('./package.json')) 2531 | }) 2532 | 2533 | const response = await fastify.inject({ 2534 | url: '/', 2535 | method: 'GET', 2536 | headers: { 2537 | 'accept-encoding': 'gzip' 2538 | } 2539 | }) 2540 | const file = readFileSync('./package.json', 'utf8') 2541 | const payload = zlib.gunzipSync(response.rawPayload) 2542 | t.assert.equal(response.headers.vary, 'accept-encoding') 2543 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2544 | t.assert.equal(payload.toString('utf-8'), file) 2545 | }) 2546 | 2547 | test('It should not apply `customTypes` option if the passed value is not a RegExp or Function', async (t) => { 2548 | t.plan(3) 2549 | 2550 | const fastify = Fastify() 2551 | await fastify.register(compressPlugin, { customTypes: 'x-user-header' }) 2552 | 2553 | fastify.get('/', (_request, reply) => { 2554 | reply 2555 | .type('application/x-user-header') 2556 | .send(createReadStream('./package.json')) 2557 | }) 2558 | 2559 | const response = await fastify.inject({ 2560 | url: '/', 2561 | method: 'GET', 2562 | headers: { 2563 | 'accept-encoding': 'gzip' 2564 | } 2565 | }) 2566 | t.assert.ok(!response.headers.vary) 2567 | t.assert.ok(!response.headers['content-encoding']) 2568 | t.assert.equal(response.statusCode, 200) 2569 | }) 2570 | 2571 | test('When `encodings` option is set, it should only use the registered value', async (t) => { 2572 | t.plan(3) 2573 | 2574 | const fastify = Fastify() 2575 | await fastify.register(compressPlugin, { encodings: ['deflate'] }) 2576 | 2577 | fastify.get('/', (_request, reply) => { 2578 | reply.send(createReadStream('./package.json')) 2579 | }) 2580 | 2581 | const response = await fastify.inject({ 2582 | url: '/', 2583 | method: 'GET', 2584 | headers: { 2585 | 'accept-encoding': 'br,gzip,deflate' 2586 | } 2587 | }) 2588 | t.assert.equal(response.headers.vary, 'accept-encoding') 2589 | t.assert.equal(response.headers['content-encoding'], 'deflate') 2590 | t.assert.equal(response.statusCode, 200) 2591 | }) 2592 | 2593 | describe('It should send data compressed according to `brotliOptions` :', async () => { 2594 | test('when using br encoding', async (t) => { 2595 | t.plan(4) 2596 | const brotliOptions = { 2597 | params: { 2598 | [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, 2599 | [zlib.constants.BROTLI_PARAM_QUALITY]: 8 2600 | } 2601 | } 2602 | 2603 | const fastify = Fastify() 2604 | await fastify.register(compressPlugin, { global: true, brotliOptions }) 2605 | 2606 | fastify.get('/', (_request, reply) => { 2607 | reply 2608 | .type('text/plain') 2609 | .compress(createReadStream('./package.json')) 2610 | }) 2611 | 2612 | const response = await fastify.inject({ 2613 | url: '/', 2614 | method: 'GET', 2615 | headers: { 2616 | 'accept-encoding': 'br' 2617 | } 2618 | }) 2619 | 2620 | const file = readFileSync('./package.json', 'utf8') 2621 | const payload = zlib.brotliDecompressSync(response.rawPayload, brotliOptions) 2622 | t.assert.equal(response.headers.vary, 'accept-encoding') 2623 | t.assert.equal(response.headers['content-encoding'], 'br') 2624 | t.assert.equal(payload.toString('utf-8'), file) 2625 | 2626 | const compressedPayload = zlib.brotliCompressSync(file, brotliOptions) 2627 | t.assert.deepEqual(response.rawPayload, compressedPayload) 2628 | }) 2629 | 2630 | test('default BROTLI_PARAM_QUALITY to be 4', async (t) => { 2631 | t.plan(1) 2632 | 2633 | const fastify = Fastify() 2634 | await fastify.register(compressPlugin, { global: true }) 2635 | 2636 | const file = readFileSync('./package.json', 'utf8') 2637 | fastify.get('/', (_request, reply) => { 2638 | reply 2639 | .type('text/plain') 2640 | .compress(file) 2641 | }) 2642 | 2643 | const response = await fastify.inject({ 2644 | url: '/', 2645 | method: 'GET', 2646 | headers: { 2647 | 'accept-encoding': 'br' 2648 | } 2649 | }) 2650 | 2651 | const defaultBrotliOptions = { 2652 | params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } 2653 | } 2654 | const compressedPayload = zlib.brotliCompressSync(file, defaultBrotliOptions) 2655 | t.assert.deepEqual(response.rawPayload, compressedPayload) 2656 | }) 2657 | }) 2658 | 2659 | describe('It should send data compressed according to `zlibOptions` :', async () => { 2660 | test('when using deflate encoding', async (t) => { 2661 | t.plan(3) 2662 | 2663 | const zlibOptions = { 2664 | level: 1, 2665 | dictionary: Buffer.from('fastifycompress') 2666 | } 2667 | 2668 | const fastify = Fastify() 2669 | await fastify.register(compressPlugin, { 2670 | global: true, 2671 | zlibOptions 2672 | }) 2673 | 2674 | fastify.get('/', (_request, reply) => { 2675 | reply 2676 | .type('text/plain') 2677 | .compress(createReadStream('./package.json')) 2678 | }) 2679 | 2680 | const response = await fastify.inject({ 2681 | url: '/', 2682 | method: 'GET', 2683 | headers: { 2684 | 'accept-encoding': 'deflate' 2685 | } 2686 | }) 2687 | const file = readFileSync('./package.json') 2688 | t.assert.equal(response.headers.vary, 'accept-encoding') 2689 | t.assert.equal(response.headers['content-encoding'], 'deflate') 2690 | t.assert.deepEqual(response.rawPayload, zlib.deflateSync(file, zlibOptions)) 2691 | }) 2692 | 2693 | test('when using gzip encoding', async (t) => { 2694 | t.plan(3) 2695 | 2696 | const zlibOptions = { level: 1 } 2697 | 2698 | const fastify = Fastify() 2699 | await fastify.register(compressPlugin, { 2700 | global: true, 2701 | zlibOptions 2702 | }) 2703 | 2704 | fastify.get('/', (_request, reply) => { 2705 | reply 2706 | .type('text/plain') 2707 | .compress(createReadStream('./package.json')) 2708 | }) 2709 | 2710 | const response = await fastify.inject({ 2711 | url: '/', 2712 | method: 'GET', 2713 | headers: { 2714 | 'accept-encoding': 'gzip' 2715 | } 2716 | }) 2717 | const file = readFileSync('./package.json') 2718 | t.assert.equal(response.headers.vary, 'accept-encoding') 2719 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2720 | t.assert.deepEqual(response.rawPayload, zlib.gzipSync(file, zlibOptions)) 2721 | }) 2722 | }) 2723 | 2724 | test('It should concat `accept-encoding` to `Vary` reply header if present', async (t) => { 2725 | t.plan(2) 2726 | 2727 | const fastify = Fastify() 2728 | await fastify.register(compressPlugin, { global: true }) 2729 | 2730 | fastify.get('/', (_request, reply) => { 2731 | reply 2732 | .header('vary', 'different-header') 2733 | .type('text/plain') 2734 | .compress(createReadStream('./package.json')) 2735 | }) 2736 | 2737 | fastify.get('/foo', (_request, reply) => { 2738 | reply 2739 | .header('vary', ['different-header', 'my-header']) 2740 | .type('text/plain') 2741 | .compress(createReadStream('./package.json')) 2742 | }) 2743 | 2744 | await fastify.inject({ 2745 | url: '/', 2746 | method: 'GET', 2747 | headers: { 2748 | 'accept-encoding': 'deflate' 2749 | } 2750 | }).then((response) => { 2751 | t.assert.deepEqual(response.headers.vary, 'different-header, accept-encoding') 2752 | }).catch((err) => { 2753 | t.error(err) 2754 | }) 2755 | 2756 | await fastify.inject({ 2757 | url: '/foo', 2758 | method: 'GET', 2759 | headers: { 2760 | 'accept-encoding': 'deflate' 2761 | } 2762 | }).then((response) => { 2763 | t.assert.deepEqual(response.headers.vary, 'different-header, my-header, accept-encoding') 2764 | }).catch((err) => { 2765 | t.error(err) 2766 | }) 2767 | }) 2768 | 2769 | test('It should not add `accept-encoding` to `Vary` reply header if already present', async (t) => { 2770 | t.plan(2) 2771 | 2772 | const fastify = Fastify() 2773 | await fastify.register(compressPlugin, { global: true }) 2774 | 2775 | fastify.get('/', (_request, reply) => { 2776 | reply 2777 | .header('vary', 'accept-encoding,different-header') 2778 | .type('text/plain') 2779 | .compress(createReadStream('./package.json')) 2780 | }) 2781 | 2782 | fastify.get('/foo', (_request, reply) => { 2783 | reply 2784 | .header('vary', 'accept-encoding, different-header, my-header') 2785 | .type('text/plain') 2786 | .compress(createReadStream('./package.json')) 2787 | }) 2788 | 2789 | await fastify.inject({ 2790 | url: '/', 2791 | method: 'GET', 2792 | headers: { 2793 | 'accept-encoding': 'deflate' 2794 | } 2795 | }).then((response) => { 2796 | t.assert.deepEqual(response.headers.vary, 'accept-encoding,different-header') 2797 | }).catch((err) => { 2798 | t.error(err) 2799 | }) 2800 | 2801 | await fastify.inject({ 2802 | url: '/foo', 2803 | method: 'GET', 2804 | headers: { 2805 | 'accept-encoding': 'deflate' 2806 | } 2807 | }).then((response) => { 2808 | t.assert.deepEqual(response.headers.vary, 'accept-encoding, different-header, my-header') 2809 | }).catch((err) => { 2810 | t.error(err) 2811 | }) 2812 | }) 2813 | 2814 | test('It should follow the `Accept-Encoding` request header encoding order', async (t) => { 2815 | t.plan(3) 2816 | 2817 | const fastify = Fastify() 2818 | await fastify.register(compressPlugin, { global: true }) 2819 | 2820 | fastify.get('/', (_request, reply) => { 2821 | reply 2822 | .type('text/plain') 2823 | .compress(createReadStream('./package.json')) 2824 | }) 2825 | 2826 | const response = await fastify.inject({ 2827 | url: '/', 2828 | method: 'GET', 2829 | headers: { 2830 | 'accept-encoding': 'invalid,br,gzip' 2831 | } 2832 | }) 2833 | const file = readFileSync('./package.json', 'utf8') 2834 | const payload = zlib.brotliDecompressSync(response.rawPayload) 2835 | t.assert.equal(response.headers.vary, 'accept-encoding') 2836 | t.assert.equal(response.headers['content-encoding'], 'br') 2837 | t.assert.equal(payload.toString('utf-8'), file) 2838 | }) 2839 | 2840 | test('It should sort and follow custom `encodings` options', async (t) => { 2841 | t.plan(3) 2842 | 2843 | const fastify = Fastify() 2844 | await fastify.register(compressPlugin, { 2845 | global: true, 2846 | encodings: ['br', 'gzip'] 2847 | }) 2848 | 2849 | fastify.get('/', (_request, reply) => { 2850 | reply 2851 | .type('text/plain') 2852 | .compress(createReadStream('./package.json')) 2853 | }) 2854 | 2855 | const response = await fastify.inject({ 2856 | url: '/', 2857 | method: 'GET', 2858 | headers: { 2859 | 'accept-encoding': 'hello,gzip,br' 2860 | } 2861 | }) 2862 | const file = readFileSync('./package.json', 'utf8') 2863 | const payload = zlib.brotliDecompressSync(response.rawPayload) 2864 | t.assert.equal(response.headers.vary, 'accept-encoding') 2865 | t.assert.equal(response.headers['content-encoding'], 'br') 2866 | t.assert.equal(payload.toString('utf-8'), file) 2867 | }) 2868 | 2869 | test('It should sort and prefer the order of custom `encodings` options', async (t) => { 2870 | t.plan(3) 2871 | 2872 | const fastify = Fastify() 2873 | await fastify.register(compressPlugin, { 2874 | global: true, 2875 | encodings: ['gzip', 'deflate', 'br'] 2876 | }) 2877 | 2878 | fastify.get('/', (_request, reply) => { 2879 | reply 2880 | .type('text/plain') 2881 | .compress(createReadStream('./package.json')) 2882 | }) 2883 | 2884 | const response = await fastify.inject({ 2885 | url: '/', 2886 | method: 'GET', 2887 | headers: { 2888 | 'accept-encoding': 'hello,gzip,br' 2889 | } 2890 | }) 2891 | 2892 | const file = readFileSync('./package.json', 'utf8') 2893 | const payload = zlib.gunzipSync(response.rawPayload) 2894 | t.assert.equal(response.headers.vary, 'accept-encoding') 2895 | t.assert.equal(response.headers['content-encoding'], 'gzip') 2896 | t.assert.equal(payload.toString('utf-8'), file) 2897 | }) 2898 | 2899 | test('It should sort and follow custom `requestEncodings` options', async (t) => { 2900 | t.plan(3) 2901 | 2902 | const fastify = Fastify() 2903 | await fastify.register(compressPlugin, { 2904 | global: true, 2905 | requestEncodings: ['gzip', 'br'] 2906 | }) 2907 | 2908 | fastify.get('/', (_request, reply) => { 2909 | reply 2910 | .type('text/plain') 2911 | .compress(createReadStream('./package.json')) 2912 | }) 2913 | 2914 | const response = await fastify.inject({ 2915 | url: '/', 2916 | method: 'GET', 2917 | headers: { 2918 | 'accept-encoding': 'hello,gzip,br' 2919 | } 2920 | }) 2921 | const file = readFileSync('./package.json', 'utf8') 2922 | const payload = zlib.brotliDecompressSync(response.rawPayload) 2923 | t.assert.equal(response.headers.vary, 'accept-encoding') 2924 | t.assert.equal(response.headers['content-encoding'], 'br') 2925 | t.assert.equal(payload.toString('utf-8'), file) 2926 | }) 2927 | 2928 | describe('It should uncompress data when `Accept-Encoding` request header is missing :', async () => { 2929 | test('using the fallback Node.js `zlib.createInflate()` method', async (t) => { 2930 | t.plan(4) 2931 | 2932 | const fastify = Fastify() 2933 | await fastify.register(compressPlugin, { 2934 | global: true, 2935 | inflateIfDeflated: true, 2936 | threshold: 0, 2937 | zlib: true // will trigger a fallback on the default zlib.createInflate 2938 | }) 2939 | 2940 | const json = { hello: 'world' } 2941 | fastify.get('/', (_request, reply) => { 2942 | reply.send(zlib.deflateSync(JSON.stringify(json))) 2943 | }) 2944 | 2945 | const response = await fastify.inject({ 2946 | url: '/', 2947 | method: 'GET' 2948 | }) 2949 | t.assert.equal(response.statusCode, 200) 2950 | t.assert.ok(!response.headers.vary) 2951 | t.assert.ok(!response.headers['content-encoding']) 2952 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 2953 | }) 2954 | 2955 | test('using the fallback Node.js `zlib.createGunzip()` method', async (t) => { 2956 | t.plan(4) 2957 | 2958 | const fastify = Fastify() 2959 | await fastify.register(compressPlugin, { 2960 | global: true, 2961 | inflateIfDeflated: true, 2962 | threshold: 0, 2963 | zlib: true // will trigger a fallback on the default zlib.createGunzip 2964 | }) 2965 | 2966 | const json = { hello: 'world' } 2967 | fastify.get('/', (_request, reply) => { 2968 | reply.send(zlib.gzipSync(JSON.stringify(json))) 2969 | }) 2970 | 2971 | const response = await fastify.inject({ 2972 | url: '/', 2973 | method: 'GET' 2974 | }) 2975 | t.assert.equal(response.statusCode, 200) 2976 | t.assert.ok(!response.headers.vary) 2977 | t.assert.ok(!response.headers['content-encoding']) 2978 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 2979 | }) 2980 | 2981 | test('when the data is a deflated Buffer', async (t) => { 2982 | t.plan(4) 2983 | 2984 | const fastify = Fastify() 2985 | await fastify.register(compressPlugin, { 2986 | global: true, 2987 | inflateIfDeflated: true, 2988 | threshold: 0 2989 | }) 2990 | 2991 | const json = { hello: 'world' } 2992 | fastify.get('/', (_request, reply) => { 2993 | reply.send(zlib.deflateSync(JSON.stringify(json))) 2994 | }) 2995 | 2996 | const response = await fastify.inject({ 2997 | url: '/', 2998 | method: 'GET' 2999 | }) 3000 | t.assert.equal(response.statusCode, 200) 3001 | t.assert.ok(!response.headers.vary) 3002 | t.assert.ok(!response.headers['content-encoding']) 3003 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 3004 | }) 3005 | 3006 | test('when the data is a gzipped Buffer', async (t) => { 3007 | t.plan(4) 3008 | 3009 | const fastify = Fastify() 3010 | await fastify.register(compressPlugin, { 3011 | global: true, 3012 | inflateIfDeflated: true, 3013 | threshold: 0 3014 | }) 3015 | 3016 | const json = { hello: 'world' } 3017 | fastify.get('/', (_request, reply) => { 3018 | reply.send(zlib.gzipSync(JSON.stringify(json))) 3019 | }) 3020 | 3021 | const response = await fastify.inject({ 3022 | url: '/', 3023 | method: 'GET' 3024 | }) 3025 | t.assert.equal(response.statusCode, 200) 3026 | t.assert.ok(!response.headers.vary) 3027 | t.assert.ok(!response.headers['content-encoding']) 3028 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 3029 | }) 3030 | 3031 | test('when the data is a deflated Stream', async (t) => { 3032 | t.plan(4) 3033 | 3034 | const fastify = Fastify() 3035 | await fastify.register(compressPlugin, { 3036 | global: true, 3037 | inflateIfDeflated: true, 3038 | threshold: 0 3039 | }) 3040 | 3041 | fastify.get('/', (_request, reply) => { 3042 | reply.send( 3043 | createReadStream('./package.json') 3044 | .pipe(zlib.createDeflate()) 3045 | ) 3046 | }) 3047 | 3048 | const response = await fastify.inject({ 3049 | url: '/', 3050 | method: 'GET' 3051 | }) 3052 | const file = readFileSync('./package.json', 'utf8') 3053 | t.assert.equal(response.statusCode, 200) 3054 | t.assert.ok(!response.headers.vary) 3055 | t.assert.ok(!response.headers['content-encoding']) 3056 | t.assert.equal(response.rawPayload.toString('utf-8'), file) 3057 | }) 3058 | 3059 | test('when the data is a gzipped Stream', async (t) => { 3060 | t.plan(4) 3061 | 3062 | const fastify = Fastify() 3063 | await fastify.register(compressPlugin, { 3064 | global: true, 3065 | inflateIfDeflated: true, 3066 | threshold: 0 3067 | }) 3068 | 3069 | fastify.get('/', (_request, reply) => { 3070 | reply.send( 3071 | createReadStream('./package.json') 3072 | .pipe(zlib.createGzip()) 3073 | ) 3074 | }) 3075 | 3076 | const response = await fastify.inject({ 3077 | url: '/', 3078 | method: 'GET' 3079 | }) 3080 | const file = readFileSync('./package.json', 'utf8') 3081 | t.assert.equal(response.statusCode, 200) 3082 | t.assert.ok(!response.headers.vary) 3083 | t.assert.ok(!response.headers['content-encoding']) 3084 | t.assert.equal(response.rawPayload.toString('utf-8'), file) 3085 | }) 3086 | 3087 | test('when the data has been compressed multiple times', async (t) => { 3088 | t.plan(4) 3089 | 3090 | const fastify = Fastify() 3091 | await fastify.register(compressPlugin, { 3092 | global: true, 3093 | inflateIfDeflated: true, 3094 | threshold: 0 3095 | }) 3096 | 3097 | const json = { hello: 'world' } 3098 | fastify.get('/', (_request, reply) => { 3099 | reply.send( 3100 | [0, 1, 2, 3, 4, 5, 6].reduce( 3101 | (x) => zlib.gzipSync(x), JSON.stringify(json) 3102 | ) 3103 | ) 3104 | }) 3105 | 3106 | const response = await fastify.inject({ 3107 | url: '/', 3108 | method: 'GET' 3109 | }) 3110 | t.assert.equal(response.statusCode, 200) 3111 | t.assert.ok(!response.headers.vary) 3112 | t.assert.ok(!response.headers['content-encoding']) 3113 | t.assert.deepEqual(JSON.parse('' + response.payload), json) 3114 | }) 3115 | }) 3116 | 3117 | describe('When `onUnsupportedEncoding` is set and the `Accept-Encoding` request header value is an unsupported encoding', async () => { 3118 | test('it should call the defined `onUnsupportedEncoding()` method', async (t) => { 3119 | t.plan(3) 3120 | 3121 | const fastify = Fastify() 3122 | await fastify.register(compressPlugin, { 3123 | global: true, 3124 | onUnsupportedEncoding: (encoding, _request, reply) => { 3125 | reply.code(406) 3126 | return JSON.stringify({ hello: encoding }) 3127 | } 3128 | }) 3129 | 3130 | fastify.get('/', (_request, reply) => { 3131 | reply 3132 | .type('text/plain') 3133 | .compress(createReadStream('./package.json')) 3134 | }) 3135 | 3136 | const response = await fastify.inject({ 3137 | url: '/', 3138 | method: 'GET', 3139 | headers: { 3140 | 'accept-encoding': 'hello' 3141 | } 3142 | }) 3143 | t.assert.equal(response.statusCode, 406) 3144 | t.assert.ok(!response.headers.vary) 3145 | t.assert.deepEqual(JSON.parse(response.payload), { hello: 'hello' }) 3146 | }) 3147 | 3148 | test('it should call the defined `onUnsupportedEncoding()` method and throw an error', async (t) => { 3149 | t.plan(2) 3150 | 3151 | const fastify = Fastify() 3152 | await fastify.register(compressPlugin, { 3153 | global: true, 3154 | onUnsupportedEncoding: (_encoding, _request, reply) => { 3155 | reply.code(406) 3156 | throw new Error('testing error') 3157 | } 3158 | }) 3159 | 3160 | fastify.get('/', (_request, reply) => { 3161 | reply 3162 | .type('text/plain') 3163 | .compress(createReadStream('./package.json')) 3164 | }) 3165 | 3166 | const response = await fastify.inject({ 3167 | url: '/', 3168 | method: 'GET', 3169 | headers: { 3170 | 'accept-encoding': 'hello' 3171 | } 3172 | }) 3173 | t.assert.equal(response.statusCode, 406) 3174 | t.assert.deepEqual(JSON.parse(response.payload), { 3175 | error: 'Not Acceptable', 3176 | message: 'testing error', 3177 | statusCode: 406 3178 | }) 3179 | }) 3180 | }) 3181 | 3182 | describe('It should error :', async () => { 3183 | test('when `encodings` array is empty', async (t) => { 3184 | t.plan(1) 3185 | 3186 | const fastify = Fastify() 3187 | fastify.register(compressPlugin, { encodings: [] }) 3188 | await t.assert.rejects(async () => fastify.ready(), { 3189 | name: 'Error', 3190 | message: 'The `encodings` option array must have at least 1 item.' 3191 | }) 3192 | }) 3193 | 3194 | test('when no entries in `encodings` are supported', (t) => { 3195 | t.plan(1) 3196 | 3197 | const fastify = Fastify() 3198 | fastify.register(compressPlugin, { 3199 | encodings: ['(not-a-real-encoding)'] 3200 | }) 3201 | 3202 | t.assert.rejects(async () => fastify.ready(), { 3203 | name: 'Error', 3204 | message: 'None of the passed `encodings` were supported — compression not possible.' 3205 | }) 3206 | }) 3207 | }) 3208 | 3209 | test('It should return an error when using `reply.compress()` with a missing payload', async (t) => { 3210 | t.plan(2) 3211 | 3212 | const fastify = Fastify() 3213 | await fastify.register(compressPlugin, { global: true }) 3214 | 3215 | fastify.get('/', (_request, reply) => { 3216 | reply.compress() 3217 | }) 3218 | 3219 | const response = await fastify.inject({ 3220 | url: '/', 3221 | method: 'GET' 3222 | }) 3223 | const payload = JSON.parse(response.payload) 3224 | t.assert.equal(response.statusCode, 500) 3225 | t.assert.deepEqual({ 3226 | error: 'Internal Server Error', 3227 | message: 'Internal server error', 3228 | statusCode: 500 3229 | }, payload) 3230 | }) 3231 | 3232 | const defaultSupportedContentTypes = [ 3233 | 'application/json', 3234 | 'application/json; charset=utf-8', 3235 | 'application/graphql-response+json', 3236 | 'application/graphql-response+json; charset=utf-8', 3237 | 'application/xml', 3238 | 'application/xml; charset=utf-8', 3239 | 'octet-stream', 3240 | 'text/xml', 3241 | 'text/xml; charset=utf-8' 3242 | ] 3243 | 3244 | for (const contentType of defaultSupportedContentTypes) { 3245 | test(`It should compress data if content-type is supported by default, ${contentType}`, async (t) => { 3246 | t.plan(3) 3247 | const fastify = Fastify() 3248 | await fastify.register(compressPlugin) 3249 | 3250 | fastify.get('/', (_request, reply) => { 3251 | reply 3252 | .type(contentType) 3253 | .send(createReadStream('./package.json')) 3254 | }) 3255 | 3256 | const response = await fastify.inject({ 3257 | url: '/', 3258 | method: 'GET', 3259 | headers: { 3260 | 'accept-encoding': 'gzip' 3261 | } 3262 | }) 3263 | const file = readFileSync('./package.json', 'utf8') 3264 | const payload = zlib.gunzipSync(response.rawPayload) 3265 | t.assert.equal(response.headers.vary, 'accept-encoding') 3266 | t.assert.equal(response.headers['content-encoding'], 'gzip') 3267 | t.assert.equal(payload.toString('utf-8'), file) 3268 | }) 3269 | } 3270 | 3271 | const notByDefaultSupportedContentTypes = [ 3272 | 'application/fastify', 3273 | 'text/event-stream' 3274 | ] 3275 | 3276 | for (const contentType of notByDefaultSupportedContentTypes) { 3277 | test(`It should not compress data if content-type is not supported by default, ${contentType}`, async (t) => { 3278 | t.plan(3) 3279 | const fastify = Fastify() 3280 | await fastify.register(compressPlugin) 3281 | 3282 | fastify.get('/', (_request, reply) => { 3283 | reply 3284 | .type(contentType) 3285 | .send(createReadStream('./package.json')) 3286 | }) 3287 | 3288 | const response = await fastify.inject({ 3289 | url: '/', 3290 | method: 'GET', 3291 | headers: { 3292 | 'accept-encoding': 'gzip' 3293 | } 3294 | }) 3295 | const file = readFileSync('./package.json', 'utf8') 3296 | t.assert.ok(!response.headers.vary) 3297 | t.assert.ok(!response.headers['content-encoding']) 3298 | t.assert.equal(response.rawPayload.toString('utf-8'), file) 3299 | }) 3300 | } 3301 | -------------------------------------------------------------------------------- /test/global-decompress.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, describe } = require('node:test') 4 | const { createReadStream } = require('node:fs') 5 | const path = require('node:path') 6 | const zlib = require('node:zlib') 7 | const pump = require('pump') 8 | const Fastify = require('fastify') 9 | const compressPlugin = require('../index') 10 | 11 | function createPayload (compressor) { 12 | let payload = createReadStream(path.resolve(__dirname, '../package.json')) 13 | 14 | if (compressor) { 15 | payload = pump(payload, compressor()) 16 | } 17 | 18 | return payload 19 | } 20 | 21 | describe('It should decompress the request payload :', async () => { 22 | test('using brotli algorithm when `Content-Encoding` request header value is set to `br`', async (t) => { 23 | t.plan(2) 24 | 25 | const fastify = Fastify() 26 | await fastify.register(compressPlugin) 27 | 28 | fastify.post('/', (request, reply) => { 29 | reply.send(request.body.name) 30 | }) 31 | 32 | const response = await fastify.inject({ 33 | url: '/', 34 | method: 'POST', 35 | headers: { 36 | 'content-type': 'application/json', 37 | 'content-encoding': 'br' 38 | }, 39 | payload: createPayload(zlib.createBrotliCompress) 40 | }) 41 | t.assert.equal(response.statusCode, 200) 42 | t.assert.equal(response.body, '@fastify/compress') 43 | }) 44 | 45 | test('using deflate algorithm when `Content-Encoding` request header value is set to `deflate`', async (t) => { 46 | t.plan(2) 47 | 48 | const fastify = Fastify() 49 | await fastify.register(compressPlugin) 50 | 51 | fastify.post('/', (request, reply) => { 52 | reply.send(request.body.name) 53 | }) 54 | 55 | const response = await fastify.inject({ 56 | url: '/', 57 | method: 'POST', 58 | headers: { 59 | 'content-type': 'application/json', 60 | 'content-encoding': 'deflate' 61 | }, 62 | payload: createPayload(zlib.createDeflate) 63 | }) 64 | t.assert.equal(response.statusCode, 200) 65 | t.assert.equal(response.body, '@fastify/compress') 66 | }) 67 | 68 | test('using gzip algorithm when `Content-Encoding` request header value is set to `gzip`', async (t) => { 69 | t.plan(2) 70 | 71 | const fastify = Fastify() 72 | await fastify.register(compressPlugin) 73 | 74 | fastify.post('/', (request, reply) => { 75 | reply.send(request.body.name) 76 | }) 77 | 78 | const response = await fastify.inject({ 79 | url: '/', 80 | method: 'POST', 81 | headers: { 82 | 'content-type': 'application/json', 83 | 'content-encoding': 'gzip' 84 | }, 85 | payload: createPayload(zlib.createGzip) 86 | }) 87 | t.assert.equal(response.statusCode, 200) 88 | t.assert.equal(response.body, '@fastify/compress') 89 | }) 90 | 91 | test('using the `forceRequestEncoding` provided algorithm over the `Content-Encoding` request header value', async (t) => { 92 | t.plan(2) 93 | 94 | const fastify = Fastify() 95 | await fastify.register(compressPlugin, { forceRequestEncoding: 'gzip' }) 96 | 97 | fastify.post('/', (request, reply) => { 98 | reply.send(request.body.name) 99 | }) 100 | 101 | const response = await fastify.inject({ 102 | url: '/', 103 | method: 'POST', 104 | headers: { 105 | 'content-type': 'application/json', 106 | 'content-encoding': 'deflate' 107 | }, 108 | payload: createPayload(zlib.createGzip) 109 | }) 110 | t.assert.equal(response.statusCode, 200) 111 | t.assert.equal(response.body, '@fastify/compress') 112 | }) 113 | }) 114 | 115 | describe('It should not decompress :', async () => { 116 | test('when `Content-Encoding` request header is missing', async (t) => { 117 | t.plan(2) 118 | 119 | const fastify = Fastify() 120 | await fastify.register(compressPlugin) 121 | 122 | fastify.post('/', (request, reply) => { 123 | reply.send(request.body.name) 124 | }) 125 | 126 | const response = await fastify.inject({ 127 | url: '/', 128 | method: 'POST', 129 | headers: { 130 | 'content-type': 'application/json' 131 | }, 132 | payload: createPayload() 133 | }) 134 | t.assert.equal(response.statusCode, 200) 135 | t.assert.equal(response.body, '@fastify/compress') 136 | }) 137 | 138 | test('when `Content-Encoding` request header value is set to `identity`', async (t) => { 139 | t.plan(2) 140 | 141 | const fastify = Fastify() 142 | await fastify.register(compressPlugin) 143 | 144 | fastify.post('/', (request, reply) => { 145 | reply.send(request.body.name) 146 | }) 147 | 148 | const response = await fastify.inject({ 149 | url: '/', 150 | method: 'POST', 151 | headers: { 152 | 'content-type': 'application/json', 153 | 'content-encoding': 'identity' 154 | }, 155 | payload: createPayload() 156 | }) 157 | t.assert.equal(response.statusCode, 200) 158 | t.assert.equal(response.body, '@fastify/compress') 159 | }) 160 | }) 161 | 162 | describe('It should return an error :', async () => { 163 | test('when `Content-Encoding` request header value is not supported', async (t) => { 164 | t.plan(2) 165 | 166 | const fastify = Fastify() 167 | await fastify.register(compressPlugin) 168 | 169 | fastify.post('/', (request, reply) => { 170 | reply.send(request.body.name) 171 | }) 172 | 173 | const response = await fastify.inject({ 174 | url: '/', 175 | method: 'POST', 176 | headers: { 177 | 'content-type': 'application/json', 178 | 'content-encoding': 'whatever' 179 | }, 180 | payload: createPayload(zlib.createDeflate) 181 | }) 182 | t.assert.equal(response.statusCode, 415) 183 | t.assert.deepEqual(response.json(), { 184 | statusCode: 415, 185 | code: 'FST_CP_ERR_INVALID_CONTENT_ENCODING', 186 | error: 'Unsupported Media Type', 187 | message: 'Unsupported Content-Encoding: whatever' 188 | }) 189 | }) 190 | 191 | test('when `Content-Encoding` request header value is disabled by the `requestEncodings` option', async (t) => { 192 | t.plan(2) 193 | 194 | const fastify = Fastify() 195 | await fastify.register(compressPlugin, { requestEncodings: ['br'] }) 196 | 197 | fastify.post('/', (request, reply) => { 198 | reply.send(request.body.name) 199 | }) 200 | 201 | const response = await fastify.inject({ 202 | url: '/', 203 | method: 'POST', 204 | headers: { 205 | 'content-type': 'application/json', 206 | 'content-encoding': 'gzip' 207 | }, 208 | payload: createPayload(zlib.createDeflate) 209 | }) 210 | t.assert.equal(response.statusCode, 415) 211 | t.assert.deepEqual(response.json(), { 212 | statusCode: 415, 213 | code: 'FST_CP_ERR_INVALID_CONTENT_ENCODING', 214 | error: 'Unsupported Media Type', 215 | message: 'Unsupported Content-Encoding: gzip' 216 | }) 217 | }) 218 | 219 | test('when the compressed payload is invalid according to `Content-Encoding` request header value', async (t) => { 220 | t.plan(2) 221 | 222 | const fastify = Fastify() 223 | await fastify.register(compressPlugin) 224 | 225 | fastify.post('/', (request, reply) => { 226 | reply.send(request.body.name) 227 | }) 228 | 229 | const response = await fastify.inject({ 230 | url: '/', 231 | method: 'POST', 232 | headers: { 233 | 'content-type': 'application/json', 234 | 'content-encoding': 'deflate' 235 | }, 236 | payload: createPayload(zlib.createGzip) 237 | }) 238 | t.assert.equal(response.statusCode, 400) 239 | t.assert.deepEqual(response.json(), { 240 | statusCode: 400, 241 | code: 'FST_CP_ERR_INVALID_CONTENT', 242 | error: 'Bad Request', 243 | message: 'Could not decompress the request payload using the provided encoding' 244 | }) 245 | }) 246 | }) 247 | 248 | describe('It should return the error returned by :', async () => { 249 | test('`onUnsupportedRequestEncoding`', async (t) => { 250 | t.plan(2) 251 | 252 | const fastify = Fastify() 253 | await fastify.register(compressPlugin, { 254 | onUnsupportedRequestEncoding (encoding) { 255 | return { 256 | statusCode: 400, 257 | code: 'INVALID', 258 | error: 'Bad Request', 259 | message: `We don't want to deal with ${encoding}.` 260 | } 261 | } 262 | }) 263 | 264 | fastify.post('/', (request, reply) => { 265 | reply.send(request.body.name) 266 | }) 267 | 268 | const response = await fastify.inject({ 269 | url: '/', 270 | method: 'POST', 271 | headers: { 272 | 'content-type': 'application/json', 273 | 'content-encoding': 'whatever' 274 | }, 275 | payload: createPayload(zlib.createDeflate) 276 | }) 277 | t.assert.equal(response.statusCode, 400) 278 | t.assert.deepEqual(response.json(), { 279 | statusCode: 400, 280 | code: 'INVALID', 281 | error: 'Bad Request', 282 | message: 'We don\'t want to deal with whatever.' 283 | }) 284 | }) 285 | 286 | test('`onInvalidRequestPayload`', async (t) => { 287 | t.plan(2) 288 | 289 | const fastify = Fastify() 290 | await fastify.register(compressPlugin, { 291 | onInvalidRequestPayload (encoding, _request, error) { 292 | return { 293 | statusCode: 400, 294 | code: 'INVALID', 295 | error: 'Bad Request', 296 | message: `What have you sent us? ${encoding} ${error.message}.` 297 | } 298 | } 299 | }) 300 | 301 | fastify.post('/', (request, reply) => { 302 | reply.send(request.body.name) 303 | }) 304 | 305 | const response = await fastify.inject({ 306 | url: '/', 307 | method: 'POST', 308 | headers: { 309 | 'content-type': 'application/json', 310 | 'content-encoding': 'deflate' 311 | }, 312 | payload: createPayload(zlib.createGzip) 313 | }) 314 | t.assert.equal(response.statusCode, 400) 315 | t.assert.deepEqual(response.json(), { 316 | statusCode: 400, 317 | code: 'INVALID', 318 | error: 'Bad Request', 319 | message: 'What have you sent us? deflate incorrect header check.' 320 | }) 321 | }) 322 | }) 323 | 324 | describe('It should return the default error :', async () => { 325 | test('when `onUnsupportedRequestEncoding` throws', async (t) => { 326 | t.plan(2) 327 | 328 | const fastify = Fastify() 329 | await fastify.register(compressPlugin, { 330 | onUnsupportedRequestEncoding () { 331 | throw new Error('Kaboom!') 332 | } 333 | }) 334 | 335 | fastify.post('/', (request, reply) => { 336 | reply.send(request.body.name) 337 | }) 338 | 339 | const response = await fastify.inject({ 340 | url: '/', 341 | method: 'POST', 342 | headers: { 343 | 'content-type': 'application/json', 344 | 'content-encoding': 'whatever' 345 | }, 346 | payload: createPayload(zlib.createDeflate) 347 | }) 348 | t.assert.equal(response.statusCode, 415) 349 | t.assert.deepEqual(response.json(), { 350 | statusCode: 415, 351 | code: 'FST_CP_ERR_INVALID_CONTENT_ENCODING', 352 | error: 'Unsupported Media Type', 353 | message: 'Unsupported Content-Encoding: whatever' 354 | }) 355 | }) 356 | 357 | test('when `onInvalidRequestPayload` throws', async (t) => { 358 | t.plan(2) 359 | 360 | const fastify = Fastify() 361 | await fastify.register(compressPlugin, { 362 | onInvalidRequestPayload () { 363 | throw new Error('Kaboom!') 364 | } 365 | }) 366 | 367 | fastify.post('/', (request, reply) => { 368 | reply.send(request.body.name) 369 | }) 370 | 371 | const response = await fastify.inject({ 372 | url: '/', 373 | method: 'POST', 374 | headers: { 375 | 'content-type': 'application/json', 376 | 'content-encoding': 'deflate' 377 | }, 378 | payload: createPayload(zlib.createGzip) 379 | }) 380 | t.assert.equal(response.statusCode, 400) 381 | t.assert.deepEqual(response.json(), { 382 | statusCode: 400, 383 | code: 'FST_CP_ERR_INVALID_CONTENT', 384 | error: 'Bad Request', 385 | message: 'Could not decompress the request payload using the provided encoding' 386 | }) 387 | }) 388 | }) 389 | 390 | test('It should validate `requestEncodings` option', async (t) => { 391 | t.plan(1) 392 | 393 | const fastify = Fastify() 394 | fastify.register(compressPlugin, { requestEncodings: [] }) 395 | t.assert.rejects( 396 | async () => fastify.ready(), 397 | { 398 | name: 'Error', 399 | message: 'The `requestEncodings` option array must have at least 1 item.' 400 | } 401 | ) 402 | }) 403 | 404 | describe('It should make sure that at least one encoding value is supported :', async () => { 405 | test('when setting `requestEncodings`', async (t) => { 406 | t.plan(1) 407 | 408 | const fastify = Fastify() 409 | fastify.register(compressPlugin, { requestEncodings: ['whatever'] }) 410 | 411 | t.assert.rejects( 412 | async () => fastify.ready(), 413 | { 414 | name: 'Error', 415 | message: 'None of the passed `requestEncodings` were supported — request decompression not possible.' 416 | } 417 | ) 418 | }) 419 | 420 | test('when setting `forceRequestEncodings`', async (t) => { 421 | t.plan(1) 422 | 423 | const fastify = Fastify() 424 | fastify.register(compressPlugin, { forceRequestEncoding: ['whatever'] }) 425 | t.assert.rejects( 426 | async () => fastify.ready(), 427 | { 428 | name: 'Error', 429 | message: 'Unsupported decompression encoding whatever.' 430 | } 431 | ) 432 | }) 433 | }) 434 | -------------------------------------------------------------------------------- /test/regression/issue-288.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const fastifyCompress = require('../..') 6 | const { request } = require('node:http') 7 | 8 | function fetch (url) { 9 | return new Promise(function (resolve, reject) { 10 | request(url, function (response) { 11 | // we need to use Buffer.concat to prevent wrong utf8 handling 12 | let body = Buffer.from('') 13 | response.on('data', function (chunk) { 14 | body = Buffer.concat([body, Buffer.from(chunk, 'utf-8')]) 15 | }) 16 | response.once('error', reject) 17 | response.once('end', function () { 18 | resolve(body.toString()) 19 | }) 20 | }) 21 | .once('error', reject) 22 | .end() 23 | }) 24 | } 25 | 26 | test('should not corrupt the file content', async (t) => { 27 | t.plan(2) 28 | 29 | // provide 2 byte unicode content 30 | const twoByteUnicodeContent = new Array(5_000) 31 | .fill('0') 32 | .map(() => { 33 | const random = new Array(10).fill('A').join('🍃') 34 | return random + '- FASTIFY COMPRESS,🍃 FASTIFY COMPRESS' 35 | }) 36 | .join('\n') 37 | const fastify = new Fastify() 38 | t.after(() => fastify.close()) 39 | 40 | fastify.register(async (instance) => { 41 | await fastify.register(fastifyCompress) 42 | // compression 43 | instance.get('/compress', async () => { 44 | return twoByteUnicodeContent 45 | }) 46 | }) 47 | 48 | // no compression 49 | fastify.get('/no-compress', async () => { 50 | return twoByteUnicodeContent 51 | }) 52 | 53 | const address = await fastify.listen({ port: 0, host: '127.0.0.1' }) 54 | 55 | const [body1, body2] = await Promise.all([ 56 | fetch(`${address}/compress`), 57 | fetch(`${address}/no-compress`) 58 | ]) 59 | 60 | t.assert.equal(body1, body2) 61 | t.assert.equal(body1, twoByteUnicodeContent) 62 | }) 63 | -------------------------------------------------------------------------------- /test/routes-compress.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, describe } = require('node:test') 4 | const { createReadStream, readFile, readFileSync } = require('node:fs') 5 | const zlib = require('node:zlib') 6 | const Fastify = require('fastify') 7 | const compressPlugin = require('../index') 8 | 9 | describe('When using routes `compress` settings :', async () => { 10 | test('it should compress data using the route custom provided `createDeflate` method', async (t) => { 11 | t.plan(12) 12 | const equal = t.assert.equal 13 | 14 | let usedCustomGlobal = false 15 | let usedCustom = false 16 | const customZlibGlobal = { createDeflate: () => (usedCustomGlobal = true) && zlib.createDeflate() } 17 | const customZlib = { createDeflate: () => (usedCustom = true) && zlib.createDeflate() } 18 | 19 | const fastify = Fastify() 20 | await fastify.register(compressPlugin, { global: true, zlib: customZlibGlobal }) 21 | 22 | fastify.get('/', (_request, reply) => { 23 | reply 24 | .type('text/plain') 25 | .compress(createReadStream('./package.json')) 26 | }) 27 | 28 | fastify.get('/custom', { 29 | compress: { zlib: customZlib } 30 | }, (_request, reply) => { 31 | reply 32 | .type('text/plain') 33 | .compress(createReadStream('./package.json')) 34 | }) 35 | 36 | await fastify.inject({ 37 | url: '/', 38 | method: 'GET', 39 | headers: { 40 | 'accept-encoding': 'deflate' 41 | } 42 | }).then((response) => { 43 | equal(usedCustom, false) 44 | equal(usedCustomGlobal, true) 45 | 46 | const file = readFileSync('./package.json', 'utf8') 47 | const payload = zlib.inflateSync(response.rawPayload) 48 | equal(response.headers.vary, 'accept-encoding') 49 | equal(response.headers['content-encoding'], 'deflate') 50 | t.assert.ok(!response.headers['content-length'], 'no content length') 51 | equal(payload.toString('utf-8'), file) 52 | 53 | usedCustom = false 54 | usedCustomGlobal = false 55 | }) 56 | 57 | const response = await fastify.inject({ 58 | url: '/custom', 59 | method: 'GET', 60 | headers: { 61 | 'accept-encoding': 'deflate' 62 | } 63 | }) 64 | equal(usedCustom, true) 65 | equal(usedCustomGlobal, false) 66 | 67 | const file = readFileSync('./package.json', 'utf8') 68 | const payload = zlib.inflateSync(response.rawPayload) 69 | equal(response.headers.vary, 'accept-encoding') 70 | equal(response.headers['content-encoding'], 'deflate') 71 | t.assert.ok(!response.headers['content-length'], 'no content length') 72 | equal(payload.toString('utf-8'), file) 73 | }) 74 | 75 | test('it should compress data using the route custom provided `createGzip` method', async (t) => { 76 | t.plan(10) 77 | const equal = t.assert.equal 78 | 79 | let usedCustomGlobal = false 80 | let usedCustom = false 81 | const customZlibGlobal = { createGzip: () => (usedCustomGlobal = true) && zlib.createGzip() } 82 | const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() } 83 | 84 | const fastify = Fastify() 85 | await fastify.register(compressPlugin, { global: false, zlib: customZlibGlobal }) 86 | 87 | fastify.get('/', (_request, reply) => { 88 | reply 89 | .type('text/plain') 90 | .compress(createReadStream('./package.json')) 91 | }) 92 | 93 | fastify.get('/custom', { compress: { zlib: customZlib } }, (_request, reply) => { 94 | reply 95 | .type('text/plain') 96 | .compress(createReadStream('./package.json')) 97 | }) 98 | 99 | await fastify.inject({ 100 | url: '/', 101 | method: 'GET', 102 | headers: { 103 | 'accept-encoding': 'gzip' 104 | } 105 | }).then((response) => { 106 | equal(usedCustom, false) 107 | equal(usedCustomGlobal, true) 108 | 109 | const file = readFileSync('./package.json', 'utf8') 110 | const payload = zlib.gunzipSync(response.rawPayload) 111 | equal(response.headers.vary, 'accept-encoding') 112 | equal(response.headers['content-encoding'], 'gzip') 113 | equal(payload.toString('utf-8'), file) 114 | 115 | usedCustom = false 116 | usedCustomGlobal = false 117 | }) 118 | 119 | const response = await fastify.inject({ 120 | url: '/custom', 121 | method: 'GET', 122 | headers: { 123 | 'accept-encoding': 'gzip' 124 | } 125 | }) 126 | equal(usedCustom, true) 127 | equal(usedCustomGlobal, false) 128 | 129 | const file = readFileSync('./package.json', 'utf8') 130 | const payload = zlib.gunzipSync(response.rawPayload) 131 | equal(response.headers.vary, 'accept-encoding') 132 | equal(response.headers['content-encoding'], 'gzip') 133 | equal(payload.toString('utf-8'), file) 134 | }) 135 | 136 | test('it should not compress data when `global` is `false` unless `compress` routes settings have been set up', async (t) => { 137 | t.plan(12) 138 | const equal = t.assert.equal 139 | 140 | let usedCustom = false 141 | const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() } 142 | 143 | const fastify = Fastify() 144 | await fastify.register(compressPlugin, { global: false }) 145 | 146 | fastify.get('/', (_request, reply) => { 147 | // compress function should still be available 148 | t.assert.ok(typeof reply.compress === 'function') 149 | 150 | reply.send({ foo: 1 }) 151 | }) 152 | 153 | fastify.get('/custom', { 154 | compress: { zlib: customZlib } 155 | }, (_request, reply) => { 156 | reply 157 | .type('text/plain') 158 | .compress(createReadStream('./package.json')) 159 | }) 160 | 161 | fastify.get('/standard', { 162 | compress: { threshold: 1 } 163 | }, (_request, reply) => { 164 | reply.send({ foo: 1 }) 165 | }) 166 | 167 | await fastify.inject({ 168 | url: '/', 169 | method: 'GET', 170 | headers: { 171 | 'accept-encoding': 'gzip' 172 | } 173 | }).then((response) => { 174 | t.assert.equal(usedCustom, false) 175 | t.assert.ok(!response.headers.vary) 176 | t.assert.ok(!response.headers['content-encoding']) 177 | t.assert.equal(response.rawPayload.toString('utf-8'), JSON.stringify({ foo: 1 })) 178 | 179 | usedCustom = false 180 | }) 181 | 182 | await fastify.inject({ 183 | url: '/custom', 184 | method: 'GET', 185 | headers: { 186 | 'accept-encoding': 'gzip' 187 | } 188 | }).then((response) => { 189 | equal(usedCustom, true) 190 | 191 | const file = readFileSync('./package.json', 'utf8') 192 | const payload = zlib.gunzipSync(response.rawPayload) 193 | equal(response.headers.vary, 'accept-encoding') 194 | equal(response.headers['content-encoding'], 'gzip') 195 | equal(payload.toString('utf-8'), file) 196 | }) 197 | 198 | const response = await fastify.inject({ 199 | url: '/standard', 200 | method: 'GET', 201 | headers: { 202 | 'accept-encoding': 'gzip' 203 | } 204 | }) 205 | const payload = zlib.gunzipSync(response.rawPayload) 206 | equal(response.headers.vary, 'accept-encoding') 207 | equal(response.headers['content-encoding'], 'gzip') 208 | equal(payload.toString('utf-8'), JSON.stringify({ foo: 1 })) 209 | }) 210 | 211 | test('it should not compress data when route `compress` option is set to `false`', async (t) => { 212 | t.plan(3) 213 | const equal = t.assert.equal 214 | const content = { message: 'Hello World!' } 215 | 216 | const fastify = Fastify() 217 | await fastify.register(compressPlugin, { global: false }) 218 | 219 | fastify.get('/', { 220 | compress: false 221 | }, (_request, reply) => { 222 | reply.send(content) 223 | }) 224 | 225 | const response = await fastify.inject({ 226 | url: '/', 227 | method: 'GET', 228 | headers: { 229 | 'accept-encoding': 'gzip' 230 | } 231 | }) 232 | 233 | t.assert.ok(!response.headers.vary) 234 | t.assert.ok(!response.headers['content-encoding']) 235 | equal(response.rawPayload.toString('utf-8'), JSON.stringify(content)) 236 | }) 237 | 238 | test('it should throw an error on invalid route `compress` settings', async (t) => { 239 | t.plan(1) 240 | const equal = t.assert.equal 241 | 242 | const fastify = Fastify() 243 | await fastify.register(compressPlugin, { global: false }) 244 | 245 | try { 246 | fastify.get('/', { 247 | compress: 'bad config' 248 | }, (_request, reply) => { 249 | reply.send('') 250 | }) 251 | } catch (err) { 252 | equal(err.message, 'Unknown value for route compress configuration') 253 | } 254 | }) 255 | }) 256 | 257 | describe('When `compress.removeContentLengthHeader` is `false`, it should not remove `Content-Length` header :', async () => { 258 | test('using `reply.compress()`', async (t) => { 259 | t.plan(4) 260 | const equal = t.assert.equal 261 | 262 | const fastify = Fastify() 263 | await fastify.register(compressPlugin, { global: true }) 264 | 265 | fastify.get('/', { 266 | compress: { removeContentLengthHeader: false } 267 | }, (_request, reply) => { 268 | readFile('./package.json', 'utf8', (err, data) => { 269 | if (err) { 270 | return reply.send(err) 271 | } 272 | 273 | reply 274 | .type('text/plain') 275 | .header('content-length', '' + data.length) 276 | .compress(data) 277 | }) 278 | }) 279 | 280 | const response = await fastify.inject({ 281 | url: '/', 282 | method: 'GET', 283 | headers: { 284 | 'accept-encoding': 'deflate' 285 | } 286 | }) 287 | const file = readFileSync('./package.json', 'utf8') 288 | const payload = zlib.inflateSync(response.rawPayload) 289 | equal(response.headers.vary, 'accept-encoding') 290 | equal(response.headers['content-encoding'], 'deflate') 291 | equal(response.headers['content-length'], payload.length.toString()) 292 | equal(payload.toString('utf-8'), file) 293 | }) 294 | 295 | test('using `onSend` hook', async (t) => { 296 | t.plan(4) 297 | const equal = t.assert.equal 298 | 299 | const fastify = Fastify() 300 | await fastify.register(compressPlugin, { global: true }) 301 | 302 | fastify.get('/', { 303 | compress: { removeContentLengthHeader: false } 304 | }, (_request, reply) => { 305 | readFile('./package.json', 'utf8', (err, data) => { 306 | if (err) { 307 | return reply.send(err) 308 | } 309 | 310 | reply 311 | .type('text/plain') 312 | .header('content-length', '' + data.length) 313 | .send(data) 314 | }) 315 | }) 316 | 317 | const response = await fastify.inject({ 318 | url: '/', 319 | method: 'GET', 320 | headers: { 321 | 'accept-encoding': 'deflate' 322 | } 323 | }) 324 | const file = readFileSync('./package.json', 'utf8') 325 | const payload = zlib.inflateSync(response.rawPayload) 326 | equal(response.headers.vary, 'accept-encoding') 327 | equal(response.headers['content-encoding'], 'deflate') 328 | equal(response.headers['content-length'], payload.length.toString()) 329 | equal(payload.toString('utf-8'), file) 330 | }) 331 | }) 332 | 333 | describe('When using the old routes `{ config: compress }` option :', async () => { 334 | test('it should compress data using the route custom provided `createGzip` method', async (t) => { 335 | t.plan(10) 336 | const equal = t.assert.equal 337 | 338 | let usedCustomGlobal = false 339 | let usedCustom = false 340 | const customZlibGlobal = { createGzip: () => (usedCustomGlobal = true) && zlib.createGzip() } 341 | const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() } 342 | 343 | const fastify = Fastify() 344 | await fastify.register(compressPlugin, { global: false, zlib: customZlibGlobal }) 345 | 346 | fastify.get('/', (_request, reply) => { 347 | reply 348 | .type('text/plain') 349 | .compress(createReadStream('./package.json')) 350 | }) 351 | 352 | fastify.get('/custom', { 353 | config: { 354 | compress: { zlib: customZlib } 355 | } 356 | }, (_request, reply) => { 357 | reply 358 | .type('text/plain') 359 | .compress(createReadStream('./package.json')) 360 | }) 361 | 362 | await fastify.inject({ 363 | url: '/', 364 | method: 'GET', 365 | headers: { 366 | 'accept-encoding': 'gzip' 367 | } 368 | }).then((response) => { 369 | equal(usedCustom, false) 370 | equal(usedCustomGlobal, true) 371 | 372 | const file = readFileSync('./package.json', 'utf8') 373 | const payload = zlib.gunzipSync(response.rawPayload) 374 | equal(response.headers.vary, 'accept-encoding') 375 | equal(response.headers['content-encoding'], 'gzip') 376 | equal(payload.toString('utf-8'), file) 377 | 378 | usedCustom = false 379 | usedCustomGlobal = false 380 | }) 381 | 382 | const response = await fastify.inject({ 383 | url: '/custom', 384 | method: 'GET', 385 | headers: { 386 | 'accept-encoding': 'gzip' 387 | } 388 | }) 389 | equal(usedCustom, true) 390 | equal(usedCustomGlobal, false) 391 | 392 | const file = readFileSync('./package.json', 'utf8') 393 | const payload = zlib.gunzipSync(response.rawPayload) 394 | equal(response.headers.vary, 'accept-encoding') 395 | equal(response.headers['content-encoding'], 'gzip') 396 | equal(payload.toString('utf-8'), file) 397 | }) 398 | 399 | test('it should use the old routes `{ config: compress }` options over routes `compress` options', async (t) => { 400 | t.plan(1) 401 | 402 | const fastify = Fastify() 403 | await fastify.register(compressPlugin, { global: false }) 404 | 405 | try { 406 | fastify.get('/', { 407 | compress: { 408 | zlib: { createGzip: () => zlib.createGzip() } 409 | }, 410 | config: { 411 | compress: 'bad config' 412 | } 413 | }, (_request, reply) => { 414 | reply.send('') 415 | }) 416 | } catch (err) { 417 | t.assert.equal(err.message, 'Unknown value for route compress configuration') 418 | } 419 | }) 420 | }) 421 | 422 | test('It should avoid to trigger `onSend` hook twice', async (t) => { 423 | t.plan(1) 424 | 425 | const server = Fastify() 426 | await server.register(compressPlugin, { threshold: 0 }) 427 | 428 | await server.register(async function (server) { 429 | server.get('/', async () => { 430 | return { hi: true } 431 | }) 432 | }, { prefix: '/test' }) 433 | 434 | const response = await server.inject({ 435 | url: '/test', 436 | method: 'GET', 437 | headers: { 438 | 'accept-encoding': 'br' 439 | } 440 | }) 441 | t.assert.deepEqual(JSON.parse(zlib.brotliDecompressSync(response.rawPayload)), { hi: true }) 442 | }) 443 | -------------------------------------------------------------------------------- /test/routes-decompress.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test, describe } = require('node:test') 4 | const { createReadStream } = require('node:fs') 5 | const path = require('node:path') 6 | const zlib = require('node:zlib') 7 | const pump = require('pump') 8 | const Fastify = require('fastify') 9 | const compressPlugin = require('../index') 10 | 11 | function createPayload (compressor) { 12 | let payload = createReadStream(path.resolve(__dirname, '../package.json')) 13 | 14 | if (compressor) { 15 | payload = pump(payload, compressor()) 16 | } 17 | 18 | return payload 19 | } 20 | 21 | describe('When using routes `decompress` settings :', async () => { 22 | test('it should decompress data using the route custom provided `createInflate` method', async (t) => { 23 | t.plan(8) 24 | const equal = t.assert.equal 25 | 26 | let usedCustomGlobal = false 27 | let usedCustom = false 28 | const customZlibGlobal = { createInflate: () => (usedCustomGlobal = true) && zlib.createInflate() } 29 | const customZlib = { createInflate: () => (usedCustom = true) && zlib.createInflate() } 30 | 31 | const fastify = Fastify() 32 | await fastify.register(compressPlugin, { zlib: customZlibGlobal }) 33 | 34 | fastify.post('/', (request, reply) => { 35 | reply.send(request.body.name) 36 | }) 37 | 38 | fastify.post('/custom', { 39 | decompress: { 40 | zlib: customZlib 41 | } 42 | }, (request, reply) => { 43 | reply.send(request.body.name) 44 | }) 45 | 46 | await fastify.inject({ 47 | url: '/', 48 | method: 'POST', 49 | headers: { 50 | 'content-type': 'application/json', 51 | 'content-encoding': 'deflate' 52 | }, 53 | payload: createPayload(zlib.createDeflate) 54 | }).then((response) => { 55 | equal(usedCustom, false) 56 | equal(usedCustomGlobal, true) 57 | 58 | equal(response.statusCode, 200) 59 | equal(response.body, '@fastify/compress') 60 | 61 | usedCustom = false 62 | usedCustomGlobal = false 63 | }) 64 | const response = await fastify.inject({ 65 | url: '/custom', 66 | method: 'POST', 67 | headers: { 68 | 'content-type': 'application/json', 69 | 'content-encoding': 'deflate' 70 | }, 71 | payload: createPayload(zlib.createDeflate) 72 | }) 73 | equal(usedCustom, true) 74 | equal(usedCustomGlobal, false) 75 | 76 | equal(response.statusCode, 200) 77 | equal(response.body, '@fastify/compress') 78 | }) 79 | 80 | test('it should decompress data using the route custom provided `createGunzip` method', async (t) => { 81 | t.plan(8) 82 | const equal = t.assert.equal 83 | 84 | let usedCustomGlobal = false 85 | let usedCustom = false 86 | const customZlibGlobal = { createGunzip: () => (usedCustomGlobal = true) && zlib.createGunzip() } 87 | const customZlib = { createGunzip: () => (usedCustom = true) && zlib.createGunzip() } 88 | 89 | const fastify = Fastify() 90 | await fastify.register(compressPlugin, { zlib: customZlibGlobal }) 91 | 92 | fastify.post('/', (request, reply) => { 93 | reply.send(request.body.name) 94 | }) 95 | 96 | fastify.post('/custom', { 97 | decompress: { 98 | zlib: customZlib 99 | } 100 | }, (request, reply) => { 101 | reply.send(request.body.name) 102 | }) 103 | 104 | await fastify.inject({ 105 | url: '/', 106 | method: 'POST', 107 | headers: { 108 | 'content-type': 'application/json', 109 | 'content-encoding': 'gzip' 110 | }, 111 | payload: createPayload(zlib.createGzip) 112 | }).then((response) => { 113 | equal(usedCustom, false) 114 | equal(usedCustomGlobal, true) 115 | 116 | equal(response.statusCode, 200) 117 | equal(response.body, '@fastify/compress') 118 | 119 | usedCustom = false 120 | usedCustomGlobal = false 121 | }) 122 | 123 | const response = await fastify.inject({ 124 | url: '/custom', 125 | method: 'POST', 126 | headers: { 127 | 'content-type': 'application/json', 128 | 'content-encoding': 'gzip' 129 | }, 130 | payload: createPayload(zlib.createGzip) 131 | }) 132 | equal(usedCustom, true) 133 | equal(usedCustomGlobal, false) 134 | 135 | equal(response.statusCode, 200) 136 | equal(response.body, '@fastify/compress') 137 | }) 138 | 139 | test('it should not decompress data when route `decompress` option is set to `false`', async (t) => { 140 | t.plan(6) 141 | const equal = t.assert.equal 142 | 143 | let usedCustomGlobal = false 144 | const customZlibGlobal = { createGunzip: () => (usedCustomGlobal = true) && zlib.createGunzip() } 145 | 146 | const fastify = Fastify() 147 | await fastify.register(compressPlugin, { zlib: customZlibGlobal }) 148 | 149 | fastify.post('/', (request, reply) => { 150 | reply.send(request.body.name) 151 | }) 152 | 153 | fastify.post('/custom', { decompress: false }, (request, reply) => { 154 | reply.send(request.body.name) 155 | }) 156 | 157 | await fastify.inject({ 158 | url: '/', 159 | method: 'POST', 160 | headers: { 161 | 'content-type': 'application/json', 162 | 'content-encoding': 'gzip' 163 | }, 164 | payload: createPayload(zlib.createGzip) 165 | }).then((response) => { 166 | equal(usedCustomGlobal, true) 167 | 168 | equal(response.statusCode, 200) 169 | equal(response.body, '@fastify/compress') 170 | 171 | usedCustomGlobal = false 172 | }) 173 | 174 | const response = await fastify.inject({ 175 | url: '/custom', 176 | method: 'POST', 177 | headers: { 178 | 'content-type': 'application/json', 179 | 'content-encoding': 'gzip' 180 | }, 181 | payload: createPayload(zlib.createGzip) 182 | }) 183 | equal(usedCustomGlobal, false) 184 | 185 | equal(response.statusCode, 400) 186 | t.assert.deepEqual(response.json(), { 187 | statusCode: 400, 188 | code: 'FST_ERR_CTP_INVALID_CONTENT_LENGTH', 189 | error: 'Bad Request', 190 | message: 'Request body size did not match Content-Length' 191 | }) 192 | }) 193 | 194 | test('it should throw an error on invalid route `decompress` settings', async (t) => { 195 | t.plan(1) 196 | 197 | const fastify = Fastify() 198 | await fastify.register(compressPlugin, { global: false }) 199 | 200 | try { 201 | fastify.post('/', { decompress: 'bad config' }, (request, reply) => { 202 | reply.send(request.body.name) 203 | }) 204 | } catch (err) { 205 | t.assert.equal(err.message, 'Unknown value for route decompress configuration') 206 | } 207 | }) 208 | }) 209 | 210 | describe('When using the old routes `{ config: decompress }` option :', async () => { 211 | test('it should decompress data using the route custom provided `createGunzip` method', async (t) => { 212 | t.plan(8) 213 | const equal = t.assert.equal 214 | 215 | let usedCustomGlobal = false 216 | let usedCustom = false 217 | const customZlibGlobal = { createGunzip: () => (usedCustomGlobal = true) && zlib.createGunzip() } 218 | const customZlib = { createGunzip: () => (usedCustom = true) && zlib.createGunzip() } 219 | 220 | const fastify = Fastify() 221 | await fastify.register(compressPlugin, { zlib: customZlibGlobal }) 222 | 223 | fastify.post('/', (request, reply) => { 224 | reply.send(request.body.name) 225 | }) 226 | 227 | fastify.post('/custom', { 228 | config: { 229 | decompress: { 230 | zlib: customZlib 231 | } 232 | } 233 | }, (request, reply) => { 234 | reply.send(request.body.name) 235 | }) 236 | 237 | await fastify.inject({ 238 | url: '/', 239 | method: 'POST', 240 | headers: { 241 | 'content-type': 'application/json', 242 | 'content-encoding': 'gzip' 243 | }, 244 | payload: createPayload(zlib.createGzip) 245 | }).then((response) => { 246 | equal(usedCustom, false) 247 | equal(usedCustomGlobal, true) 248 | 249 | equal(response.statusCode, 200) 250 | equal(response.body, '@fastify/compress') 251 | 252 | usedCustom = false 253 | usedCustomGlobal = false 254 | }) 255 | 256 | const response = await fastify.inject({ 257 | url: '/custom', 258 | method: 'POST', 259 | headers: { 260 | 'content-type': 'application/json', 261 | 'content-encoding': 'gzip' 262 | }, 263 | payload: createPayload(zlib.createGzip) 264 | }) 265 | equal(usedCustom, true) 266 | equal(usedCustomGlobal, false) 267 | 268 | equal(response.statusCode, 200) 269 | equal(response.body, '@fastify/compress') 270 | }) 271 | 272 | test('it should use the old routes `{ config: decompress }` options over routes `decompress` options', async (t) => { 273 | t.plan(1) 274 | 275 | const fastify = Fastify() 276 | await fastify.register(compressPlugin, { global: false }) 277 | 278 | try { 279 | fastify.post('/', { 280 | decompress: { 281 | zlib: { createGunzip: () => zlib.createGunzip() } 282 | }, 283 | config: { 284 | decompress: 'bad config' 285 | } 286 | }, (request, reply) => { 287 | reply.send(request.body.name) 288 | }) 289 | } catch (err) { 290 | t.assert.equal(err.message, 'Unknown value for route decompress configuration') 291 | } 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { createReadStream } = require('node:fs') 4 | const { Socket } = require('node:net') 5 | const { Duplex, PassThrough, Readable, Stream, Transform, Writable } = require('node:stream') 6 | const { test } = require('node:test') 7 | const { isStream, isDeflate, isGzip, intoAsyncIterator } = require('../lib/utils') 8 | 9 | test('isStream() utility should be able to detect Streams', async (t) => { 10 | t.plan(12) 11 | const equal = t.assert.equal 12 | 13 | equal(isStream(new Stream()), true) 14 | equal(isStream(new Readable()), true) 15 | equal(isStream(new Writable()), true) 16 | equal(isStream(new Duplex()), true) 17 | equal(isStream(new Transform()), true) 18 | equal(isStream(new PassThrough()), true) 19 | 20 | equal(isStream(createReadStream('package.json')), true) 21 | 22 | equal(isStream(new Socket()), true) 23 | 24 | equal(isStream({}), false) 25 | equal(isStream(null), false) 26 | equal(isStream(undefined), false) 27 | equal(isStream(''), false) 28 | }) 29 | 30 | test('isDeflate() utility should be able to detect deflate compressed Buffer', async (t) => { 31 | t.plan(14) 32 | const equal = t.assert.equal 33 | 34 | equal(isDeflate(Buffer.alloc(0)), false) 35 | equal(isDeflate(Buffer.alloc(0)), false) 36 | equal(isDeflate(Buffer.from([0x78])), false) 37 | equal(isDeflate(Buffer.from([0x78, 0x00])), false) 38 | equal(isDeflate(Buffer.from([0x7a, 0x01])), false) 39 | equal(isDeflate(Buffer.from([0x88, 0x01])), false) 40 | equal(isDeflate(Buffer.from([0x78, 0x11])), false) 41 | equal(isDeflate(Buffer.from([0x78, 0x01])), true) 42 | equal(isDeflate(Buffer.from([0x78, 0x9c])), true) 43 | equal(isDeflate(Buffer.from([0x78, 0xda])), true) 44 | 45 | equal(isDeflate({}), false) 46 | equal(isDeflate(null), false) 47 | equal(isDeflate(undefined), false) 48 | equal(isDeflate(''), false) 49 | }) 50 | 51 | test('isGzip() utility should be able to detect gzip compressed Buffer', async (t) => { 52 | t.plan(10) 53 | const equal = t.assert.equal 54 | 55 | equal(isGzip(Buffer.alloc(0)), false) 56 | equal(isGzip(Buffer.alloc(1)), false) 57 | equal(isGzip(Buffer.alloc(2)), false) 58 | equal(isGzip(Buffer.from([0x1f, 0x8b])), false) 59 | equal(isGzip(Buffer.from([0x1f, 0x8b, 0x00])), false) 60 | equal(isGzip(Buffer.from([0x1f, 0x8b, 0x08])), true) 61 | 62 | equal(isGzip({}), false) 63 | equal(isGzip(null), false) 64 | equal(isGzip(undefined), false) 65 | equal(isGzip(''), false) 66 | }) 67 | 68 | test('intoAsyncIterator() utility should handle different data', async (t) => { 69 | t.plan(8) 70 | const equal = t.assert.equal 71 | 72 | const buf = Buffer.from('foo') 73 | const str = 'foo' 74 | const arr = [str, str] 75 | const arrayBuffer = new ArrayBuffer(8) 76 | const typedArray = new Int32Array(arrayBuffer) 77 | const asyncIterator = (async function * () { 78 | yield str 79 | })() 80 | const obj = {} 81 | 82 | for await (const buffer of intoAsyncIterator(buf)) { 83 | equal(buffer, buf) 84 | } 85 | 86 | for await (const string of intoAsyncIterator(str)) { 87 | equal(string, str) 88 | } 89 | 90 | for await (const chunk of intoAsyncIterator(arr)) { 91 | equal(chunk, str) 92 | } 93 | 94 | for await (const chunk of intoAsyncIterator(arrayBuffer)) { 95 | equal(chunk.toString(), Buffer.from(arrayBuffer).toString()) 96 | } 97 | 98 | for await (const chunk of intoAsyncIterator(typedArray)) { 99 | equal(chunk.toString(), Buffer.from(typedArray).toString()) 100 | } 101 | 102 | for await (const chunk of intoAsyncIterator(asyncIterator)) { 103 | equal(chunk, str) 104 | } 105 | 106 | for await (const chunk of intoAsyncIterator(obj)) { 107 | equal(chunk, obj) 108 | } 109 | }) 110 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FastifyPluginCallback, 3 | FastifyReply, 4 | FastifyRequest, 5 | RouteOptions as FastifyRouteOptions, 6 | RawServerBase, 7 | RawServerDefault 8 | } from 'fastify' 9 | import { Stream } from 'node:stream' 10 | import { BrotliOptions, ZlibOptions } from 'node:zlib' 11 | 12 | declare module 'fastify' { 13 | export interface FastifyContextConfig { 14 | /** @deprecated `config.compress` is deprecated, use `compress` shorthand option instead */ 15 | compress?: RouteCompressOptions | false; 16 | /** @deprecated `config.decompress` is deprecated, use `decompress` shorthand option instead */ 17 | decompress?: RouteDecompressOptions | false; 18 | } 19 | 20 | export interface RouteShorthandOptions< 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | RawServer extends RawServerBase = RawServerDefault 23 | > { 24 | compress?: RouteCompressOptions | false; 25 | decompress?: RouteDecompressOptions | false; 26 | } 27 | 28 | interface FastifyReply { 29 | compress(input: Stream | Input): void; 30 | } 31 | 32 | export interface RouteOptions { 33 | compress?: RouteCompressOptions | false; 34 | decompress?: RouteDecompressOptions | false; 35 | } 36 | } 37 | 38 | type FastifyCompress = FastifyPluginCallback 39 | 40 | type RouteCompressOptions = Pick 51 | 52 | type RouteDecompressOptions = Pick 59 | 60 | type EncodingToken = 'br' | 'deflate' | 'gzip' | 'identity' 61 | 62 | type CompressibleContentTypeFunction = (contentType: string) => boolean 63 | 64 | type Input = 65 | | Buffer 66 | | NodeJS.TypedArray 67 | | ArrayBuffer 68 | | string 69 | | Iterable 70 | | AsyncIterable 71 | 72 | declare namespace fastifyCompress { 73 | 74 | export interface FastifyCompressOptions { 75 | brotliOptions?: BrotliOptions; 76 | customTypes?: RegExp | CompressibleContentTypeFunction; 77 | encodings?: EncodingToken[]; 78 | forceRequestEncoding?: EncodingToken; 79 | global?: boolean; 80 | inflateIfDeflated?: boolean; 81 | onInvalidRequestPayload?: (encoding: string, request: FastifyRequest, error: Error) => Error | undefined | null; 82 | onUnsupportedEncoding?: (encoding: string, request: FastifyRequest, reply: FastifyReply) => string | Buffer | Stream; 83 | onUnsupportedRequestEncoding?: (encoding: string, request: FastifyRequest, reply: FastifyReply) => Error | undefined | null; 84 | removeContentLengthHeader?: boolean; 85 | requestEncodings?: EncodingToken[]; 86 | threshold?: number; 87 | zlib?: unknown; 88 | zlibOptions?: ZlibOptions; 89 | } 90 | 91 | export interface FastifyCompressRouteOptions { 92 | compress?: RouteCompressOptions | false; 93 | decompress?: RouteDecompressOptions | false; 94 | } 95 | 96 | export interface RouteOptions extends FastifyRouteOptions, FastifyCompressRouteOptions { } 97 | 98 | export interface RoutesConfigCompressOptions { 99 | /** @deprecated `config.compress` is deprecated, use `compress` shorthand option instead */ 100 | compress?: RouteCompressOptions | false; 101 | /** @deprecated `config.decompress` is deprecated, use `decompress` shorthand option instead */ 102 | decompress?: RouteDecompressOptions | false; 103 | } 104 | 105 | export const fastifyCompress: FastifyCompress 106 | export { fastifyCompress as default } 107 | } 108 | 109 | declare function fastifyCompress (...params: Parameters): ReturnType 110 | export = fastifyCompress 111 | -------------------------------------------------------------------------------- /types/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from 'fastify' 2 | import { createReadStream } from 'node:fs' 3 | import { expectError, expectType } from 'tsd' 4 | import * as zlib from 'node:zlib' 5 | import fastifyCompress, { FastifyCompressOptions } from '..' 6 | 7 | const stream = createReadStream('./package.json') 8 | 9 | const withGlobalOptions: FastifyCompressOptions = { 10 | global: true, 11 | threshold: 10, 12 | zlib, 13 | brotliOptions: { 14 | params: { 15 | [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, 16 | [zlib.constants.BROTLI_PARAM_QUALITY]: 4 17 | } 18 | }, 19 | zlibOptions: { level: 1 }, 20 | inflateIfDeflated: true, 21 | customTypes: /x-protobuf$/, 22 | encodings: ['gzip', 'br', 'identity', 'deflate'], 23 | requestEncodings: ['gzip', 'br', 'identity', 'deflate'], 24 | forceRequestEncoding: 'gzip', 25 | removeContentLengthHeader: true 26 | } 27 | 28 | const app: FastifyInstance = fastify() 29 | app.register(fastifyCompress, withGlobalOptions) 30 | 31 | app.register(fastifyCompress, { 32 | customTypes: value => value === 'application/json' 33 | }) 34 | 35 | app.get('/test-one', async (_request, reply) => { 36 | expectType(reply.compress(stream)) 37 | }) 38 | 39 | app.get('/test-two', async (_request, reply) => { 40 | expectError(reply.compress()) 41 | }) 42 | 43 | // Instantiation of an app without global 44 | const appWithoutGlobal: FastifyInstance = fastify() 45 | appWithoutGlobal.register(fastifyCompress, { global: false }) 46 | 47 | appWithoutGlobal.get('/one', { 48 | compress: { 49 | zlib: { 50 | createGzip: () => zlib.createGzip() 51 | }, 52 | removeContentLengthHeader: false 53 | }, 54 | decompress: { 55 | forceRequestEncoding: 'gzip', 56 | zlib: { 57 | createGunzip: () => zlib.createGunzip() 58 | } 59 | } 60 | }, (_request, reply) => { 61 | expectType(reply.type('text/plain').compress(stream)) 62 | }) 63 | 64 | appWithoutGlobal.get('/two', { 65 | config: { 66 | compress: { 67 | zlib: { 68 | createGzip: () => zlib.createGzip() 69 | } 70 | }, 71 | decompress: { 72 | forceRequestEncoding: 'gzip', 73 | zlib: { 74 | createGunzip: () => zlib.createGunzip() 75 | } 76 | } 77 | } 78 | }, (_request, reply) => { 79 | expectType(reply.type('text/plain').compress(stream)) 80 | }) 81 | 82 | expectError( 83 | appWithoutGlobal.get('/throw-a-ts-arg-error-on-shorthand-route', { 84 | compress: 'bad compress route option value', 85 | decompress: 'bad decompress route option value' 86 | }, (_request, reply) => { 87 | expectType(reply.type('text/plain').compress(stream)) 88 | }) 89 | ) 90 | 91 | expectError( 92 | appWithoutGlobal.route({ 93 | method: 'GET', 94 | path: '/throw-a-ts-arg-error', 95 | compress: 'bad compress route option value', 96 | decompress: 'bad decompress route option value', 97 | handler: (_request, reply) => { expectType(reply.type('text/plain').compress(stream)) } 98 | }) 99 | ) 100 | 101 | appWithoutGlobal.inject( 102 | { 103 | method: 'GET', 104 | path: '/throw-a-ts-arg-error', 105 | headers: { 106 | 'accept-encoding': 'gzip' 107 | } 108 | }, 109 | (err) => { 110 | expectType(err) 111 | } 112 | ) 113 | 114 | // Instantiation of an app that should trigger a typescript error 115 | const appThatTriggerAnError = fastify() 116 | expectError(appThatTriggerAnError.register(fastifyCompress, { 117 | global: true, 118 | thisOptionDoesNotExist: 'trigger a typescript error' 119 | })) 120 | --------------------------------------------------------------------------------