@fastify/compress
Advanced tools
| name: Lock Threads | ||
| on: | ||
| schedule: | ||
| - cron: '0 0 1 * *' | ||
| workflow_dispatch: | ||
| concurrency: | ||
| group: lock | ||
| permissions: | ||
| contents: read | ||
| jobs: | ||
| lock-threads: | ||
| permissions: | ||
| issues: write | ||
| pull-requests: write | ||
| uses: fastify/workflows/.github/workflows/lock-threads.yml@v6 |
| 'use strict' | ||
| const { test } = require('node:test') | ||
| const { setTimeout: sleep } = require('node:timers/promises') | ||
| const net = require('node:net') | ||
| const { Writable } = require('node:stream') | ||
| const Fastify = require('fastify') | ||
| const fastifyCompress = require('../..') | ||
| test('should not log "premature close" errors on client disconnect (global compression)', async (t) => { | ||
| let prematureCloseCount = 0 | ||
| const fastify = Fastify({ | ||
| logger: { | ||
| level: 'error', | ||
| stream: new Writable({ | ||
| write (chunk, encoding, callback) { | ||
| if (chunk.toString().includes('premature close')) { | ||
| prematureCloseCount++ | ||
| } | ||
| callback() | ||
| } | ||
| }) | ||
| } | ||
| }) | ||
| await fastify.register(fastifyCompress, { | ||
| global: true, | ||
| encodings: ['gzip'], | ||
| threshold: 1024 | ||
| }) | ||
| // Streaming endpoint that takes time to produce data | ||
| fastify.get('/stream', async (_request, reply) => { | ||
| let chunks = 0 | ||
| const stream = new (require('node:stream').Readable)({ | ||
| read () { | ||
| if (chunks >= 20) { | ||
| this.push(null) | ||
| return | ||
| } | ||
| setTimeout(() => { | ||
| this.push(JSON.stringify({ id: chunks, data: 'x'.repeat(1000) }) + '\n') | ||
| chunks++ | ||
| }, 50) | ||
| } | ||
| }) | ||
| reply.type('text/plain') | ||
| return stream | ||
| }) | ||
| await fastify.listen({ port: 0 }) | ||
| const port = fastify.server.address().port | ||
| // Simulate clients that disconnect before the full response is sent | ||
| for (let i = 0; i < 10; i++) { | ||
| await new Promise((_resolve) => { | ||
| const sock = net.connect(port, '127.0.0.1', () => { | ||
| sock.write('GET /stream HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n') | ||
| // Disconnect after receiving partial response (100ms) | ||
| setTimeout(() => { | ||
| sock.destroy() | ||
| _resolve() | ||
| }, 100) | ||
| }) | ||
| sock.on('error', () => _resolve()) | ||
| }) | ||
| } | ||
| // Allow pending callbacks to fire | ||
| await sleep(2000) | ||
| await fastify.close() | ||
| t.assert.equal( | ||
| prematureCloseCount, | ||
| 0, | ||
| `Expected no "premature close" errors, but got ${prematureCloseCount} errors from 10 client disconnects` | ||
| ) | ||
| }) | ||
| test('should not log "premature close" errors on client disconnect (reply.compress path)', async (t) => { | ||
| let prematureCloseCount = 0 | ||
| const fastify = Fastify({ | ||
| logger: { | ||
| level: 'error', | ||
| stream: new Writable({ | ||
| write (chunk, encoding, callback) { | ||
| if (chunk.toString().includes('premature close')) { | ||
| prematureCloseCount++ | ||
| } | ||
| callback() | ||
| } | ||
| }) | ||
| } | ||
| }) | ||
| await fastify.register(fastifyCompress, { | ||
| global: false, | ||
| encodings: ['gzip'] | ||
| }) | ||
| // Streaming endpoint using reply.compress | ||
| fastify.get('/stream', async (_request, reply) => { | ||
| let chunks = 0 | ||
| const stream = new (require('node:stream').Readable)({ | ||
| read () { | ||
| if (chunks >= 20) { | ||
| this.push(null) | ||
| return | ||
| } | ||
| setTimeout(() => { | ||
| this.push(JSON.stringify({ id: chunks, data: 'x'.repeat(1000) }) + '\n') | ||
| chunks++ | ||
| }, 50) | ||
| } | ||
| }) | ||
| reply.type('text/plain') | ||
| return reply.compress(stream) | ||
| }) | ||
| await fastify.listen({ port: 0 }) | ||
| const port = fastify.server.address().port | ||
| // Simulate clients that disconnect before the full response is sent | ||
| for (let i = 0; i < 10; i++) { | ||
| await new Promise((_resolve) => { | ||
| const sock = net.connect(port, '127.0.0.1', () => { | ||
| sock.write('GET /stream HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n') | ||
| // Disconnect after receiving partial response (100ms) | ||
| setTimeout(() => { | ||
| sock.destroy() | ||
| _resolve() | ||
| }, 100) | ||
| }) | ||
| sock.on('error', () => _resolve()) | ||
| }) | ||
| } | ||
| // Allow pending callbacks to fire | ||
| await sleep(2000) | ||
| await fastify.close() | ||
| t.assert.equal( | ||
| prematureCloseCount, | ||
| 0, | ||
| `Expected no "premature close" errors, but got ${prematureCloseCount} errors from 10 client disconnects` | ||
| ) | ||
| }) | ||
| test('should still log actual errors (not premature close)', async (t) => { | ||
| const errors = [] | ||
| const fastify = Fastify({ | ||
| logger: { | ||
| level: 'error', | ||
| stream: new Writable({ | ||
| write (chunk, encoding, callback) { | ||
| const msg = chunk.toString() | ||
| if (msg.includes('premature close')) { | ||
| // Ignore premature close errors | ||
| } else { | ||
| errors.push(msg) | ||
| } | ||
| callback() | ||
| } | ||
| }) | ||
| } | ||
| }) | ||
| await fastify.register(fastifyCompress, { | ||
| global: true, | ||
| encodings: ['gzip'] | ||
| }) | ||
| // Route that throws an error | ||
| fastify.get('/error', async () => { | ||
| throw new Error('Test error') | ||
| }) | ||
| await fastify.inject({ | ||
| method: 'GET', | ||
| url: '/error', | ||
| headers: { 'accept-encoding': 'gzip' } | ||
| }) | ||
| await sleep(500) | ||
| t.assert.ok( | ||
| errors.length > 0, | ||
| 'Expected actual errors to still be logged' | ||
| ) | ||
| }) |
| import fastify, { FastifyInstance } from 'fastify' | ||
| import { createReadStream } from 'node:fs' | ||
| import { expect } from 'tstyche' | ||
| import * as zlib from 'node:zlib' | ||
| import fastifyCompress, { FastifyCompressOptions } from '..' | ||
| const stream = createReadStream('./package.json') | ||
| const withGlobalOptions: FastifyCompressOptions = { | ||
| global: true, | ||
| threshold: 10, | ||
| zlib, | ||
| brotliOptions: { | ||
| params: { | ||
| [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, | ||
| [zlib.constants.BROTLI_PARAM_QUALITY]: 4 | ||
| } | ||
| }, | ||
| zlibOptions: { level: 1 }, | ||
| inflateIfDeflated: true, | ||
| customTypes: /x-protobuf$/, | ||
| encodings: ['gzip', 'br', 'identity', 'deflate'], | ||
| requestEncodings: ['gzip', 'br', 'identity', 'deflate'], | ||
| forceRequestEncoding: 'gzip', | ||
| removeContentLengthHeader: true | ||
| } | ||
| const withZstdOptions: FastifyCompressOptions = { | ||
| encodings: ['zstd', 'br', 'gzip', 'deflate', 'identity'], | ||
| requestEncodings: ['zstd', 'br', 'gzip', 'deflate', 'identity'] | ||
| } | ||
| const app: FastifyInstance = fastify() | ||
| app.register(fastifyCompress, withGlobalOptions) | ||
| app.register(fastifyCompress, withZstdOptions) | ||
| app.register(fastifyCompress, { | ||
| customTypes: value => value === 'application/json' | ||
| }) | ||
| app.get('/test-one', async (_request, reply) => { | ||
| expect(reply.compress(stream)).type.toBe<void>() | ||
| }) | ||
| app.get('/test-two', async (_request, reply) => { | ||
| expect(reply.compress).type.not.toBeCallableWith() | ||
| }) | ||
| // Instantiation of an app without global | ||
| const appWithoutGlobal: FastifyInstance = fastify() | ||
| appWithoutGlobal.register(fastifyCompress, { global: false }) | ||
| appWithoutGlobal.get('/one', { | ||
| compress: { | ||
| zlib: { | ||
| createGzip: () => zlib.createGzip() | ||
| }, | ||
| removeContentLengthHeader: false | ||
| }, | ||
| decompress: { | ||
| forceRequestEncoding: 'gzip', | ||
| zlib: { | ||
| createGunzip: () => zlib.createGunzip() | ||
| } | ||
| } | ||
| }, (_request, reply) => { | ||
| expect(reply.type('text/plain').compress(stream)).type.toBe<void>() | ||
| }) | ||
| appWithoutGlobal.get('/two', { | ||
| config: { | ||
| compress: { | ||
| zlib: { | ||
| createGzip: () => zlib.createGzip() | ||
| } | ||
| }, | ||
| decompress: { | ||
| forceRequestEncoding: 'gzip', | ||
| zlib: { | ||
| createGunzip: () => zlib.createGunzip() | ||
| } | ||
| } | ||
| } | ||
| }, (_request, reply) => { | ||
| expect(reply.type('text/plain').compress(stream)).type.toBe<void>() | ||
| }) | ||
| appWithoutGlobal.get('/throw-a-ts-arg-error-on-shorthand-route', { | ||
| // @ts-expect-error Type 'string' is not assignable | ||
| compress: 'bad compress route option value', | ||
| // @ts-expect-error Type 'string' is not assignable | ||
| decompress: 'bad decompress route option value' | ||
| }, (_request, reply) => { | ||
| reply.type('text/plain').compress(stream) | ||
| }) | ||
| appWithoutGlobal.route({ | ||
| method: 'GET', | ||
| path: '/throw-a-ts-arg-error', | ||
| // @ts-expect-error Type 'string' is not assignable | ||
| compress: 'bad compress route option value', | ||
| // @ts-expect-error Type 'string' is not assignable | ||
| decompress: 'bad decompress route option value', | ||
| handler: (_request, reply) => { reply.type('text/plain').compress(stream) } | ||
| }) | ||
| appWithoutGlobal.inject( | ||
| { | ||
| method: 'GET', | ||
| path: '/throw-a-ts-arg-error', | ||
| headers: { | ||
| 'accept-encoding': 'gzip' | ||
| } | ||
| }, | ||
| (err) => { | ||
| expect(err).type.toBe<Error | undefined>() | ||
| } | ||
| ) | ||
| // Test that invalid encoding values trigger TypeScript errors | ||
| expect(fastify().register).type.not.toBeCallableWith(fastifyCompress, { | ||
| encodings: ['invalid-encoding'] | ||
| }) | ||
| expect(fastify().register).type.not.toBeCallableWith(fastifyCompress, { | ||
| requestEncodings: ['another-invalid-encoding'] | ||
| }) | ||
| // Instantiation of an app that should trigger a typescript error | ||
| const appThatTriggerAnError = fastify() | ||
| expect(appThatTriggerAnError.register).type.not.toBeCallableWith(fastifyCompress, { | ||
| global: true, | ||
| thisOptionDoesNotExist: 'trigger a typescript error' | ||
| }) | ||
| app.get('/ts-fetch-response', async (_request, reply) => { | ||
| const resp = new Response('ok', { headers: { 'content-type': 'text/plain' } }) | ||
| expect(reply.compress(resp)).type.toBe<void>() | ||
| }) | ||
| app.get('/ts-web-readable-stream', async (_request, reply) => { | ||
| const stream = new ReadableStream({ | ||
| start (controller) { | ||
| controller.enqueue(new Uint8Array([1, 2, 3])) | ||
| controller.close() | ||
| } | ||
| }) | ||
| expect(reply.compress(stream)).type.toBe<void>() | ||
| }) |
@@ -30,5 +30,5 @@ name: CI | ||
| pull-requests: write | ||
| uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5 | ||
| uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6 | ||
| with: | ||
| license-check: true | ||
| lint: true |
+18
-20
@@ -5,10 +5,9 @@ 'use strict' | ||
| const { inherits, format } = require('node:util') | ||
| const { pipeline, compose } = require('node:stream') | ||
| const fp = require('fastify-plugin') | ||
| const encodingNegotiator = require('@fastify/accept-negotiator') | ||
| const pump = require('pump') | ||
| const mimedb = require('mime-db') | ||
| const peek = require('peek-stream') | ||
| const { Minipass } = require('minipass') | ||
| const pumpify = require('pumpify') | ||
| const { Readable } = require('readable-stream') | ||
@@ -54,7 +53,2 @@ | ||
| fastify.addHook('onRoute', (routeOptions) => { | ||
| // If route config.compress has been set it takes precedence over compress | ||
| if (routeOptions.config?.compress !== undefined) { | ||
| routeOptions.compress = routeOptions.config.compress | ||
| } | ||
| // Manage compression options | ||
@@ -84,7 +78,2 @@ if (routeOptions.compress !== undefined) { | ||
| // If route config.decompress has been set it takes precedence over compress | ||
| if (routeOptions.config?.decompress !== undefined) { | ||
| routeOptions.decompress = routeOptions.config.decompress | ||
| } | ||
| // Manage decompression options | ||
@@ -295,3 +284,3 @@ if (routeOptions.decompress !== undefined) { | ||
| : reply.header('Content-Encoding', 'identity') | ||
| pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply)) | ||
| pipeline(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(reply)) | ||
| } | ||
@@ -323,3 +312,3 @@ return next(null, payload) | ||
| stream = zipStream(params.compressStream, encoding) | ||
| pump(payload, stream, onEnd.bind(reply)) | ||
| pipeline(payload, stream, onEnd.bind(reply)) | ||
| next(null, stream) | ||
@@ -386,3 +375,6 @@ } | ||
| next(null, pump(raw, decompresser)) | ||
| pipeline(raw, decompresser, () => { | ||
| // Cleanup callback - decompression errors are handled by decompresser's error handler | ||
| }) | ||
| next(null, decompresser) | ||
| } | ||
@@ -424,3 +416,3 @@ } | ||
| : this.header('Content-Encoding', 'identity') | ||
| pump(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this)) | ||
| pipeline(stream, payload = unzipStream(params.uncompressStream), onEnd.bind(this)) | ||
| } | ||
@@ -456,3 +448,3 @@ return this.send(payload) | ||
| stream = zipStream(params.compressStream, encoding) | ||
| pump(payload, stream, onEnd.bind(this)) | ||
| pipeline(payload, stream, onEnd.bind(this)) | ||
| return this.send(stream) | ||
@@ -475,3 +467,9 @@ } | ||
| function onEnd (err) { | ||
| if (err) this.log.error(err) | ||
| // Client disconnection during streaming is expected and handled by Fastify. | ||
| // Do not log "premature close" errors at error level since they are not | ||
| // actual errors - they occur when clients disconnect mid-response. | ||
| // See: https://github.com/fastify/fastify-compress/issues/382 | ||
| if (err && err.message !== 'premature close') { | ||
| this.log.error(err) | ||
| } | ||
| } | ||
@@ -577,4 +575,4 @@ | ||
| switch (isCompressed(data)) { | ||
| case 1: return swap(null, pumpify(inflate.gzip(), unzipStream(inflate, maxRecursion - 1))) | ||
| case 2: return swap(null, pumpify(inflate.deflate(), unzipStream(inflate, maxRecursion - 1))) | ||
| case 1: return swap(null, compose(inflate.gzip(), unzipStream(inflate, maxRecursion - 1))) | ||
| case 2: return swap(null, compose(inflate.deflate(), unzipStream(inflate, maxRecursion - 1))) | ||
| } | ||
@@ -581,0 +579,0 @@ return swap(null, new Minipass()) |
+1
-3
| MIT License | ||
| Copyright (c) 2017-present The Fastify team | ||
| Copyright (c) 2017-present The Fastify team <https://github.com/fastify/fastify#team> | ||
| The Fastify team members are listed at https://github.com/fastify/fastify#team. | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
@@ -8,0 +6,0 @@ of this software and associated documentation files (the "Software"), to deal |
+7
-12
| { | ||
| "name": "@fastify/compress", | ||
| "version": "8.3.1", | ||
| "version": "9.0.0", | ||
| "description": "Fastify compression utils", | ||
@@ -14,16 +14,14 @@ "main": "index.js", | ||
| "peek-stream": "^1.1.3", | ||
| "pump": "^3.0.0", | ||
| "pumpify": "^2.0.1", | ||
| "readable-stream": "^4.5.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^24.0.8", | ||
| "@types/node": "^25.0.3", | ||
| "adm-zip": "^0.5.12", | ||
| "c8": "^10.1.2", | ||
| "c8": "^11.0.0", | ||
| "eslint": "^9.17.0", | ||
| "fastify": "^5.0.0", | ||
| "jsonstream": "^1.0.3", | ||
| "neostandard": "^0.12.0", | ||
| "tsd": "^0.33.0", | ||
| "typescript": "~5.9.2" | ||
| "neostandard": "^0.13.0", | ||
| "tstyche": "^7.1.0", | ||
| "typescript": "~6.0.2" | ||
| }, | ||
@@ -34,3 +32,3 @@ "scripts": { | ||
| "test": "npm run test:unit && npm run test:typescript", | ||
| "test:typescript": "tsd", | ||
| "test:typescript": "tstyche", | ||
| "test:unit": "node --test", | ||
@@ -87,5 +85,2 @@ "test:coverage": "c8 node --test && c8 report --reporter=html", | ||
| }, | ||
| "tsd": { | ||
| "directory": "types" | ||
| }, | ||
| "publishConfig": { | ||
@@ -92,0 +87,0 @@ "access": "public" |
@@ -5,5 +5,5 @@ 'use strict' | ||
| const { createReadStream } = require('node:fs') | ||
| const { pipeline } = require('node:stream') | ||
| const path = require('node:path') | ||
| const zlib = require('node:zlib') | ||
| const pump = require('pump') | ||
| const Fastify = require('fastify') | ||
@@ -16,3 +16,5 @@ const compressPlugin = require('../index') | ||
| if (compressor) { | ||
| payload = pump(payload, compressor()) | ||
| const compressed = compressor() | ||
| pipeline(payload, compressed, () => {}) | ||
| payload = compressed | ||
| } | ||
@@ -19,0 +21,0 @@ |
@@ -333,91 +333,2 @@ 'use strict' | ||
| describe('When using the old routes `{ config: compress }` option :', async () => { | ||
| test('it should compress data using the route custom provided `createGzip` method', async (t) => { | ||
| t.plan(10) | ||
| const equal = t.assert.equal | ||
| let usedCustomGlobal = false | ||
| let usedCustom = false | ||
| const customZlibGlobal = { createGzip: () => (usedCustomGlobal = true) && zlib.createGzip() } | ||
| const customZlib = { createGzip: () => (usedCustom = true) && zlib.createGzip() } | ||
| const fastify = Fastify() | ||
| await fastify.register(compressPlugin, { global: false, zlib: customZlibGlobal }) | ||
| fastify.get('/', (_request, reply) => { | ||
| reply | ||
| .type('text/plain') | ||
| .compress(createReadStream('./package.json')) | ||
| }) | ||
| fastify.get('/custom', { | ||
| config: { | ||
| compress: { zlib: customZlib } | ||
| } | ||
| }, (_request, reply) => { | ||
| reply | ||
| .type('text/plain') | ||
| .compress(createReadStream('./package.json')) | ||
| }) | ||
| await fastify.inject({ | ||
| url: '/', | ||
| method: 'GET', | ||
| headers: { | ||
| 'accept-encoding': 'gzip' | ||
| } | ||
| }).then((response) => { | ||
| equal(usedCustom, false) | ||
| equal(usedCustomGlobal, true) | ||
| const file = readFileSync('./package.json', 'utf8') | ||
| const payload = zlib.gunzipSync(response.rawPayload) | ||
| equal(response.headers.vary, 'accept-encoding') | ||
| equal(response.headers['content-encoding'], 'gzip') | ||
| equal(payload.toString('utf-8'), file) | ||
| usedCustom = false | ||
| usedCustomGlobal = false | ||
| }) | ||
| const response = await fastify.inject({ | ||
| url: '/custom', | ||
| method: 'GET', | ||
| headers: { | ||
| 'accept-encoding': 'gzip' | ||
| } | ||
| }) | ||
| equal(usedCustom, true) | ||
| equal(usedCustomGlobal, false) | ||
| const file = readFileSync('./package.json', 'utf8') | ||
| const payload = zlib.gunzipSync(response.rawPayload) | ||
| equal(response.headers.vary, 'accept-encoding') | ||
| equal(response.headers['content-encoding'], 'gzip') | ||
| equal(payload.toString('utf-8'), file) | ||
| }) | ||
| test('it should use the old routes `{ config: compress }` options over routes `compress` options', async (t) => { | ||
| t.plan(1) | ||
| const fastify = Fastify() | ||
| await fastify.register(compressPlugin, { global: false }) | ||
| try { | ||
| fastify.get('/', { | ||
| compress: { | ||
| zlib: { createGzip: () => zlib.createGzip() } | ||
| }, | ||
| config: { | ||
| compress: 'bad config' | ||
| } | ||
| }, (_request, reply) => { | ||
| reply.send('') | ||
| }) | ||
| } catch (err) { | ||
| t.assert.equal(err.message, 'Unknown value for route compress configuration') | ||
| } | ||
| }) | ||
| }) | ||
| test('It should avoid to trigger `onSend` hook twice', async (t) => { | ||
@@ -424,0 +335,0 @@ t.plan(1) |
@@ -5,5 +5,5 @@ 'use strict' | ||
| const { createReadStream } = require('node:fs') | ||
| const { pipeline } = require('node:stream') | ||
| const path = require('node:path') | ||
| const zlib = require('node:zlib') | ||
| const pump = require('pump') | ||
| const Fastify = require('fastify') | ||
@@ -16,3 +16,5 @@ const compressPlugin = require('../index') | ||
| if (compressor) { | ||
| payload = pump(payload, compressor()) | ||
| const compressed = compressor() | ||
| pipeline(payload, compressed, () => {}) | ||
| payload = compressed | ||
| } | ||
@@ -236,86 +238,1 @@ | ||
| }) | ||
| describe('When using the old routes `{ config: decompress }` option :', async () => { | ||
| test('it should decompress data using the route custom provided `createGunzip` method', async (t) => { | ||
| t.plan(8) | ||
| const equal = t.assert.equal | ||
| let usedCustomGlobal = false | ||
| let usedCustom = false | ||
| const customZlibGlobal = { createGunzip: () => (usedCustomGlobal = true) && zlib.createGunzip() } | ||
| const customZlib = { createGunzip: () => (usedCustom = true) && zlib.createGunzip() } | ||
| const fastify = Fastify() | ||
| await fastify.register(compressPlugin, { zlib: customZlibGlobal }) | ||
| fastify.post('/', (request, reply) => { | ||
| reply.send(request.body.name) | ||
| }) | ||
| fastify.post('/custom', { | ||
| config: { | ||
| decompress: { | ||
| zlib: customZlib | ||
| } | ||
| } | ||
| }, (request, reply) => { | ||
| reply.send(request.body.name) | ||
| }) | ||
| await fastify.inject({ | ||
| url: '/', | ||
| method: 'POST', | ||
| headers: { | ||
| 'content-type': 'application/json', | ||
| 'content-encoding': 'gzip' | ||
| }, | ||
| payload: createPayload(zlib.createGzip) | ||
| }).then((response) => { | ||
| equal(usedCustom, false) | ||
| equal(usedCustomGlobal, true) | ||
| equal(response.statusCode, 200) | ||
| equal(response.body, '@fastify/compress') | ||
| usedCustom = false | ||
| usedCustomGlobal = false | ||
| }) | ||
| const response = await fastify.inject({ | ||
| url: '/custom', | ||
| method: 'POST', | ||
| headers: { | ||
| 'content-type': 'application/json', | ||
| 'content-encoding': 'gzip' | ||
| }, | ||
| payload: createPayload(zlib.createGzip) | ||
| }) | ||
| equal(usedCustom, true) | ||
| equal(usedCustomGlobal, false) | ||
| equal(response.statusCode, 200) | ||
| equal(response.body, '@fastify/compress') | ||
| }) | ||
| test('it should use the old routes `{ config: decompress }` options over routes `decompress` options', async (t) => { | ||
| t.plan(1) | ||
| const fastify = Fastify() | ||
| await fastify.register(compressPlugin, { global: false }) | ||
| try { | ||
| fastify.post('/', { | ||
| decompress: { | ||
| zlib: { createGunzip: () => zlib.createGunzip() } | ||
| }, | ||
| config: { | ||
| decompress: 'bad config' | ||
| } | ||
| }, (request, reply) => { | ||
| reply.send(request.body.name) | ||
| }) | ||
| } catch (err) { | ||
| t.assert.equal(err.message, 'Unknown value for route decompress configuration') | ||
| } | ||
| }) | ||
| }) |
+0
-14
@@ -13,9 +13,2 @@ import { | ||
| declare module 'fastify' { | ||
| export interface FastifyContextConfig { | ||
| /** @deprecated `config.compress` is deprecated, use `compress` shorthand option instead */ | ||
| compress?: RouteCompressOptions | false; | ||
| /** @deprecated `config.decompress` is deprecated, use `decompress` shorthand option instead */ | ||
| decompress?: RouteDecompressOptions | false; | ||
| } | ||
| export interface RouteShorthandOptions< | ||
@@ -101,9 +94,2 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| export interface RoutesConfigCompressOptions { | ||
| /** @deprecated `config.compress` is deprecated, use `compress` shorthand option instead */ | ||
| compress?: RouteCompressOptions | false; | ||
| /** @deprecated `config.decompress` is deprecated, use `decompress` shorthand option instead */ | ||
| decompress?: RouteDecompressOptions | false; | ||
| } | ||
| export const fastifyCompress: FastifyCompress | ||
@@ -110,0 +96,0 @@ export { fastifyCompress as default } |
| # Number of days of inactivity before an issue becomes stale | ||
| daysUntilStale: 15 | ||
| # Number of days of inactivity before a stale issue is closed | ||
| daysUntilClose: 7 | ||
| # Issues with these labels will never be considered stale | ||
| exemptLabels: | ||
| - "discussion" | ||
| - "feature request" | ||
| - "bug" | ||
| - "help wanted" | ||
| - "plugin suggestion" | ||
| - "good first issue" | ||
| # Label to use when marking an issue as stale | ||
| staleLabel: stale | ||
| # Comment to post when marking an issue as stale. Set to `false` to disable | ||
| markComment: > | ||
| This issue has been automatically marked as stale because it has not had | ||
| recent activity. It will be closed if no further activity occurs. Thank you | ||
| for your contributions. | ||
| # Comment to post when closing a stale issue. Set to `false` to disable | ||
| closeComment: false |
| import fastify, { FastifyInstance } from 'fastify' | ||
| import { createReadStream } from 'node:fs' | ||
| import { expectError, expectType } from 'tsd' | ||
| import * as zlib from 'node:zlib' | ||
| import fastifyCompress, { FastifyCompressOptions } from '..' | ||
| const stream = createReadStream('./package.json') | ||
| const withGlobalOptions: FastifyCompressOptions = { | ||
| global: true, | ||
| threshold: 10, | ||
| zlib, | ||
| brotliOptions: { | ||
| params: { | ||
| [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, | ||
| [zlib.constants.BROTLI_PARAM_QUALITY]: 4 | ||
| } | ||
| }, | ||
| zlibOptions: { level: 1 }, | ||
| inflateIfDeflated: true, | ||
| customTypes: /x-protobuf$/, | ||
| encodings: ['gzip', 'br', 'identity', 'deflate'], | ||
| requestEncodings: ['gzip', 'br', 'identity', 'deflate'], | ||
| forceRequestEncoding: 'gzip', | ||
| removeContentLengthHeader: true | ||
| } | ||
| const withZstdOptions: FastifyCompressOptions = { | ||
| encodings: ['zstd', 'br', 'gzip', 'deflate', 'identity'], | ||
| requestEncodings: ['zstd', 'br', 'gzip', 'deflate', 'identity'] | ||
| } | ||
| const app: FastifyInstance = fastify() | ||
| app.register(fastifyCompress, withGlobalOptions) | ||
| app.register(fastifyCompress, withZstdOptions) | ||
| app.register(fastifyCompress, { | ||
| customTypes: value => value === 'application/json' | ||
| }) | ||
| app.get('/test-one', async (_request, reply) => { | ||
| expectType<void>(reply.compress(stream)) | ||
| }) | ||
| app.get('/test-two', async (_request, reply) => { | ||
| expectError(reply.compress()) | ||
| }) | ||
| // Instantiation of an app without global | ||
| const appWithoutGlobal: FastifyInstance = fastify() | ||
| appWithoutGlobal.register(fastifyCompress, { global: false }) | ||
| appWithoutGlobal.get('/one', { | ||
| compress: { | ||
| zlib: { | ||
| createGzip: () => zlib.createGzip() | ||
| }, | ||
| removeContentLengthHeader: false | ||
| }, | ||
| decompress: { | ||
| forceRequestEncoding: 'gzip', | ||
| zlib: { | ||
| createGunzip: () => zlib.createGunzip() | ||
| } | ||
| } | ||
| }, (_request, reply) => { | ||
| expectType<void>(reply.type('text/plain').compress(stream)) | ||
| }) | ||
| appWithoutGlobal.get('/two', { | ||
| config: { | ||
| compress: { | ||
| zlib: { | ||
| createGzip: () => zlib.createGzip() | ||
| } | ||
| }, | ||
| decompress: { | ||
| forceRequestEncoding: 'gzip', | ||
| zlib: { | ||
| createGunzip: () => zlib.createGunzip() | ||
| } | ||
| } | ||
| } | ||
| }, (_request, reply) => { | ||
| expectType<void>(reply.type('text/plain').compress(stream)) | ||
| }) | ||
| expectError( | ||
| appWithoutGlobal.get('/throw-a-ts-arg-error-on-shorthand-route', { | ||
| compress: 'bad compress route option value', | ||
| decompress: 'bad decompress route option value' | ||
| }, (_request, reply) => { | ||
| expectType<void>(reply.type('text/plain').compress(stream)) | ||
| }) | ||
| ) | ||
| expectError( | ||
| appWithoutGlobal.route({ | ||
| method: 'GET', | ||
| path: '/throw-a-ts-arg-error', | ||
| compress: 'bad compress route option value', | ||
| decompress: 'bad decompress route option value', | ||
| handler: (_request, reply) => { expectType<void>(reply.type('text/plain').compress(stream)) } | ||
| }) | ||
| ) | ||
| appWithoutGlobal.inject( | ||
| { | ||
| method: 'GET', | ||
| path: '/throw-a-ts-arg-error', | ||
| headers: { | ||
| 'accept-encoding': 'gzip' | ||
| } | ||
| }, | ||
| (err) => { | ||
| expectType<Error | undefined>(err) | ||
| } | ||
| ) | ||
| // Test that invalid encoding values trigger TypeScript errors | ||
| expectError(fastify().register(fastifyCompress, { | ||
| encodings: ['invalid-encoding'] | ||
| })) | ||
| expectError(fastify().register(fastifyCompress, { | ||
| requestEncodings: ['another-invalid-encoding'] | ||
| })) | ||
| // Instantiation of an app that should trigger a typescript error | ||
| const appThatTriggerAnError = fastify() | ||
| expectError(appThatTriggerAnError.register(fastifyCompress, { | ||
| global: true, | ||
| thisOptionDoesNotExist: 'trigger a typescript error' | ||
| })) | ||
| app.get('/ts-fetch-response', async (_request, reply) => { | ||
| const resp = new Response('ok', { headers: { 'content-type': 'text/plain' } }) | ||
| expectType<void>(reply.compress(resp)) | ||
| }) | ||
| app.get('/ts-web-readable-stream', async (_request, reply) => { | ||
| const stream = new ReadableStream({ | ||
| start (controller) { | ||
| controller.enqueue(new Uint8Array([1, 2, 3])) | ||
| controller.close() | ||
| } | ||
| }) | ||
| expectType<void>(reply.compress(stream)) | ||
| }) |
Unstable ownership
Supply chain riskA new collaborator has begun publishing package versions. Package stability and security risk may be elevated.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
6
-25%21
5%5093
0.26%194623
-0.43%1
Infinity%4
33.33%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed