koa-compress 5.2.1 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/HISTORY.md CHANGED
@@ -1,4 +1,19 @@
1
- SEE https://github.com/koajs/compress/releases
1
+ 5.3.0 / 2026-11-09
2
+ ==================
3
+
4
+ * fixed `Blob` bodies, which were sent as `{}`, and compressed `Blob`, `ReadableStream`, and `Response` bodies, thx [yash](https://github.com/yxshee)
5
+ * fixed a compressed stream body that fails partway: its error reaches Koa instead of leaving the response open
6
+ * fixed a 500 when a client refuses every encoding, `identity` included: the response is sent uncompressed
7
+ * added the `rejectUnacceptable` option to respond with `406 Not Acceptable` in that case instead
8
+ * changed: `ReadableStream` and `Response` bodies without `Content-Length` are compressed regardless of `threshold`; set `ctx.compress = false` for live streams, such as server-sent events
9
+ * breaking for strict TypeScript: the `size` argument of threshold and encoding functions is typed `number | undefined`
10
+ * removed the unused `http-errors` dependency
11
+ * updated docs and dependencies
12
+
13
+ 5.2.2 / 2026-06-15
14
+ ==================
15
+
16
+ * skip compression for `206 Partial Content`, `Content-Range`, and `multipart/byteranges` responses, thx [naveentehrpariya](https://github.com/naveentehrpariya)
2
17
 
3
18
  5.2.1 / 2026-03-12
4
19
  ==================
package/README.md CHANGED
@@ -81,6 +81,14 @@ The HTTP spec treats a missing header as `*` (any encoding is acceptable),
81
81
  but that causes problems when debugging with tools like `curl` or `wget`.
82
82
  Set `defaultEncoding` to `"*"` to restore spec-compliant behavior.
83
83
 
84
+ ### options.rejectUnacceptable\<Boolean\>
85
+
86
+ What to do when the client refuses every encoding the middleware can send, `identity` included (for example, `Accept-Encoding: identity;q=0`).
87
+ By default, the response goes out uncompressed.
88
+ Set `rejectUnacceptable` to `true` to respond with `406 Not Acceptable` instead.
89
+ It applies only to responses that pass the filter and the threshold.
90
+ Default: `false`.
91
+
84
92
  ## Manually turning compression on and off
85
93
 
86
94
  You can force compression by setting `ctx.compress = true` (bypasses the filter check).
@@ -94,16 +102,31 @@ app.use((ctx, next) => {
94
102
  ```
95
103
 
96
104
  `ctx.compress` can also be an options object (same shape as the middleware options).
97
- Its `threshold` and encoding properties override the global defaults for this response.
105
+ Its `threshold`, `rejectUnacceptable`, and encoding properties override the global defaults for this response.
98
106
  Note: an options object does **not** bypass the filter check — only `true` does.
99
107
 
108
+ ### Live streams
109
+
110
+ The `threshold` applies to a stream only when you set `Content-Length`.
111
+ Otherwise, Node.js streams, `ReadableStream` bodies, and `Response` bodies are compressed whenever the filter accepts their type.
112
+ A compressor holds data until it has enough to compress, so a live stream, such as server-sent events, can reach the client late.
113
+ To send a live stream as it's written, set `ctx.compress = false`:
114
+
115
+ ```js
116
+ app.use((ctx, next) => {
117
+ ctx.compress = false;
118
+ ctx.type = "text/event-stream";
119
+ ctx.body = events; // a stream that writes each event as it happens
120
+ });
121
+ ```
122
+
100
123
  ## Functional properties
101
124
 
102
125
  The `threshold` and per-encoding options can be functions. They are called
103
126
  for every response with three arguments:
104
127
 
105
128
  - `type` &mdash; same as `ctx.response.type`
106
- - `size` &mdash; same as `ctx.response.length`
129
+ - `size` &mdash; the response size in bytes, or `undefined` for a stream without `Content-Length`
107
130
  - `ctx` &mdash; the full Koa context object
108
131
 
109
132
  The function should return a valid value for that property.
package/lib/index.d.ts CHANGED
@@ -9,8 +9,8 @@ declare namespace compress {
9
9
  type ThresholdFunction = (
10
10
  /** MIME type of the response */
11
11
  type: string,
12
- /** Size of the response in bytes */
13
- size: number,
12
+ /** Size of the response in bytes, or `undefined` for a stream without `Content-Length` */
13
+ size: number | undefined,
14
14
  /** Context of the request */
15
15
  ctx: Koa.Context,
16
16
  ) => number | string | ThresholdFunction;
@@ -22,8 +22,8 @@ declare namespace compress {
22
22
  type ZlibOptionsFunction = (
23
23
  /** MIME type of the response */
24
24
  type: string,
25
- /** Size of the response in bytes */
26
- size: number,
25
+ /** Size of the response in bytes, or `undefined` for a stream without `Content-Length` */
26
+ size: number | undefined,
27
27
  /** Context of the request */
28
28
  ctx: Koa.Context,
29
29
  ) => boolean | null | ZlibOptions | ZlibOptionsFunction;
@@ -35,8 +35,8 @@ declare namespace compress {
35
35
  type BrotliOptionsFunction = (
36
36
  /** MIME type of the response */
37
37
  type: string,
38
- /** Size of the response in bytes */
39
- size: number,
38
+ /** Size of the response in bytes, or `undefined` for a stream without `Content-Length` */
39
+ size: number | undefined,
40
40
  /** Context of the request */
41
41
  ctx: Koa.Context,
42
42
  ) => boolean | null | BrotliOptions | BrotliOptionsFunction;
@@ -48,8 +48,8 @@ declare namespace compress {
48
48
  type ZstdOptionsFunction = (
49
49
  /** MIME type of the response */
50
50
  type: string,
51
- /** Size of the response in bytes */
52
- size: number,
51
+ /** Size of the response in bytes, or `undefined` for a stream without `Content-Length` */
52
+ size: number | undefined,
53
53
  /** Context of the request */
54
54
  ctx: Koa.Context,
55
55
  ) => boolean | null | ZstdOptions | ZstdOptionsFunction;
@@ -86,6 +86,13 @@ declare namespace compress {
86
86
  * Default: `['zstd', 'br', 'gzip', 'deflate', 'identity']`.
87
87
  */
88
88
  encodingPreference?: string[];
89
+ /**
90
+ * What to do when the client refuses every encoding the middleware can
91
+ * send, `identity` included (for example, `Accept-Encoding: identity;q=0`):
92
+ * `false` sends the response uncompressed, `true` responds with
93
+ * `406 Not Acceptable`. Default: `false`.
94
+ */
95
+ rejectUnacceptable?: boolean;
89
96
  /**
90
97
  * Options passed to `zlib.createDeflate()`, or `false`/`null` to disable.
91
98
  * Can be a {@link ZlibOptionsFunction} for per-response values.
package/lib/index.js CHANGED
@@ -93,7 +93,11 @@ module.exports = (options = {}) => {
93
93
  await next()
94
94
 
95
95
  let { body } = ctx
96
- const { type, length: size } = ctx.response
96
+ let { type, length: size } = ctx.response
97
+ // koa measures web streams as the JSON '{}', but their size is unknown
98
+ if (!ctx.response.has('Content-Length') && (body instanceof ReadableStream || body instanceof Response)) {
99
+ size = undefined
100
+ }
97
101
  if (
98
102
  // early exit if there's no content body or the body is already encoded
99
103
  !body ||
@@ -101,7 +105,11 @@ module.exports = (options = {}) => {
101
105
  !ctx.writable ||
102
106
  ctx.compress === false ||
103
107
  ctx.request.method === 'HEAD' ||
104
- emptyBodyStatuses.has(+ctx.response.status) ||
108
+ emptyBodyStatuses.has(ctx.status) ||
109
+ // don't compress partial content: Content-Range describes unencoded byte offsets
110
+ ctx.status === 206 ||
111
+ ctx.response.get('Content-Range') ||
112
+ /^multipart\/byteranges\b/i.test(ctx.response.get('Content-Type') || '') ||
105
113
  ctx.response.get('Content-Encoding') ||
106
114
  // forced compression or implied
107
115
  !(ctx.compress === true || filter(type)) ||
@@ -149,16 +157,28 @@ module.exports = (options = {}) => {
149
157
  }
150
158
 
151
159
  const negotiator = new Negotiator(request)
152
- // if no encodings are supported, negotiator returns 'identity'
160
+ // undefined when the client refuses identity along with everything we offer
153
161
  const encoding = negotiator.encoding(finalEncodings, {
154
162
  preferred: encodingPreference
155
163
  })
156
164
 
165
+ if (!encoding && responseOptions.rejectUnacceptable) {
166
+ // koa drops the headers we set when it renders an error
167
+ ctx.throw(406, `Please accept one of: ${finalEncodings.join(', ')}.`, {
168
+ headers: { Vary: 'Accept-Encoding' }
169
+ })
170
+ }
171
+
157
172
  // identity === no compression
158
- if (encoding === 'identity') return
173
+ if (!encoding || encoding === 'identity') return
159
174
 
160
175
  /** begin compression logic **/
161
176
 
177
+ // web bodies supported by Koa 3: Blob, ReadableStream and Response
178
+ if (body instanceof Blob) body = body.stream()
179
+ else if (body instanceof Response) body = body.body ?? ''
180
+ if (body instanceof ReadableStream) body = Stream.Readable.from(body)
181
+
162
182
  // json
163
183
  if (isJSON(body)) body = ctx.body = JSON.stringify(body)
164
184
 
@@ -169,7 +189,8 @@ module.exports = (options = {}) => {
169
189
  const compressOptions = { ...encodingMethodDefaultOptions[encoding], ...responseOptions[encoding] }
170
190
  const stream = (ctx.body = compress(compressOptions))
171
191
 
172
- if (body instanceof Stream) return body.pipe(stream)
173
- stream.end(body)
192
+ // pipeline hands source errors to the compressor, which koa handles
193
+ if (body instanceof Stream) Stream.pipeline(body, stream, () => {})
194
+ else stream.end(body)
174
195
  }
175
196
  }
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "koa-compress",
3
- "description": "Compress middleware for koa",
4
- "version": "5.2.1",
3
+ "description": "Koa middleware that compresses HTTP responses with zstd, Brotli, gzip, or deflate",
4
+ "keywords": [
5
+ "koa",
6
+ "koajs",
7
+ "koa-middleware",
8
+ "middleware",
9
+ "compress",
10
+ "compression",
11
+ "gzip",
12
+ "deflate",
13
+ "brotli",
14
+ "zstd",
15
+ "zstandard",
16
+ "content-encoding",
17
+ "accept-encoding",
18
+ "http",
19
+ "response",
20
+ "stream"
21
+ ],
22
+ "version": "5.3.0",
5
23
  "type": "commonjs",
6
24
  "author": {
7
25
  "name": "Jonathan Ong",
@@ -22,24 +40,26 @@
22
40
  }
23
41
  ],
24
42
  "license": "MIT",
25
- "repository": "koajs/compress",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/koajs/compress.git"
46
+ },
26
47
  "dependencies": {
27
48
  "bytes": "^3.1.2",
28
49
  "compressible": "^2.0.18",
29
- "http-errors": "^2.0.1",
30
50
  "koa-is-json": "^1.0.0",
31
- "negotiator": "^1.0.0"
51
+ "negotiator": "^1.1.0"
32
52
  },
33
53
  "devDependencies": {
34
- "@types/koa": "^3.0.1",
35
- "@types/node": "^25.5.0",
36
- "c8": "^11.0.0",
37
- "jest": "^30.3.0",
38
- "koa": "^3.1.2",
54
+ "@types/koa": "^3.0.3",
55
+ "@types/node": "^26.6.6",
56
+ "c8": "^9.1.0",
57
+ "jest": "^29.7.0",
58
+ "koa": "^3.3.0",
39
59
  "snazzy": "^9.0.0",
40
60
  "standard": "^17.1.2",
41
- "supertest": "^7.2.2",
42
- "typescript": "^5.9.3"
61
+ "supertest": "^7.3.2",
62
+ "typescript": "^7.0.2"
43
63
  },
44
64
  "scripts": {
45
65
  "test": "jest",
@@ -57,9 +77,6 @@
57
77
  ],
58
78
  "main": "lib",
59
79
  "types": "lib/index.d.ts",
60
- "engines": {
61
- "node": ">= 12"
62
- },
63
80
  "standard": {
64
81
  "globals": [
65
82
  "afterAll",
@@ -72,5 +89,9 @@
72
89
  "expect",
73
90
  "it"
74
91
  ]
92
+ },
93
+ "homepage": "https://github.com/koajs/compress",
94
+ "bugs": {
95
+ "url": "https://github.com/koajs/compress/issues"
75
96
  }
76
97
  }