Skip to content

Commit 449833a

Browse files
committed
Add zstd (Zstandard) compression support
Fixes #2383
1 parent f5c54a3 commit 449833a

File tree

5 files changed

+144
-6
lines changed

5 files changed

+144
-6
lines changed

documentation/2-options.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,10 +784,13 @@ try {
784784
**Type: `boolean`**\
785785
**Default: `true`**
786786

787-
Decompress the response automatically. This will set the `accept-encoding` header to `gzip, deflate, br`.
787+
Decompress the response automatically. This will set the `accept-encoding` header to `gzip, deflate, br` (and `zstd` on Node.js >= 22.15.0).
788788

789789
If disabled, a compressed response is returned as a `Buffer`. This may be useful if you want to handle decompression yourself.
790790

791+
> [!NOTE]
792+
> Zstandard (`zstd`) compression support is available on Node.js >= 22.15.0 and will be automatically enabled when available.
793+
791794
```js
792795
import got from 'got';
793796

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
"network",
4242
"gzip",
4343
"brotli",
44+
"zstd",
45+
"zstandard",
4446
"requests",
4547
"human-friendly",
4648
"axios",
@@ -53,7 +55,7 @@
5355
"@szmarczak/http-timer": "^5.0.1",
5456
"cacheable-lookup": "^7.0.0",
5557
"cacheable-request": "^13.0.12",
56-
"decompress-response": "^6.0.0",
58+
"decompress-response": "^10.0.0",
5759
"form-data-encoder": "^4.0.2",
5860
"http2-wrapper": "^2.2.1",
5961
"keyv": "^5.5.3",
@@ -79,6 +81,7 @@
7981
"benchmark": "^2.1.4",
8082
"bluebird": "^3.7.2",
8183
"body-parser": "^1.20.3",
84+
"c8": "^10.1.3",
8285
"create-cert": "^1.0.6",
8386
"create-test-server": "^3.0.1",
8487
"del-cli": "^6.0.0",
@@ -91,7 +94,6 @@
9194
"nock": "^13.5.5",
9295
"node-fetch": "^3.3.2",
9396
"np": "^10.0.5",
94-
"c8": "^10.1.3",
9597
"p-event": "^6.0.1",
9698
"pem": "^1.14.8",
9799
"pify": "^6.1.0",

source/core/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export type Progress = {
5353
};
5454

5555
const supportsBrotli = is.string(process.versions.brotli);
56+
const supportsZstd = is.string(process.versions.zstd);
5657

5758
const methodsWithoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);
5859
// Methods that should auto-end streams when no body is provided
@@ -1263,7 +1264,16 @@ export default class Request extends Duplex implements RequestEvents<Request> {
12631264
}
12641265

12651266
if (options.decompress && is.undefined(headers['accept-encoding'])) {
1266-
headers['accept-encoding'] = supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate';
1267+
const encodings = ['gzip', 'deflate'];
1268+
if (supportsBrotli) {
1269+
encodings.push('br');
1270+
}
1271+
1272+
if (supportsZstd) {
1273+
encodings.push('zstd');
1274+
}
1275+
1276+
headers['accept-encoding'] = encodings.join(', ');
12671277
}
12681278

12691279
if (username || password) {

test/headers.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import got, {type Headers} from '../source/index.js';
1111
import withServer from './helpers/with-server.js';
1212

1313
const supportsBrotli = typeof (process.versions as any).brotli === 'string';
14+
const supportsZstd = typeof (process.versions as any).zstd === 'string';
1415

1516
const echoHeaders: Handler = (request, response) => {
1617
request.resume();
@@ -28,7 +29,16 @@ test('`accept-encoding`', withServer, async (t, server, got) => {
2829
server.get('/', echoHeaders);
2930

3031
const headers = await got('').json<Headers>();
31-
t.is(headers['accept-encoding'], supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate');
32+
const encodings = ['gzip', 'deflate'];
33+
if (supportsBrotli) {
34+
encodings.push('br');
35+
}
36+
37+
if (supportsZstd) {
38+
encodings.push('zstd');
39+
}
40+
41+
t.is(headers['accept-encoding'], encodings.join(', '));
3242
});
3343

3444
test('does not override provided `accept-encoding`', withServer, async (t, server, got) => {
@@ -53,9 +63,18 @@ test('does not remove user headers from `url` object argument', withServer, asyn
5363
},
5464
})).body;
5565

66+
const encodings = ['gzip', 'deflate'];
67+
if (supportsBrotli) {
68+
encodings.push('br');
69+
}
70+
71+
if (supportsZstd) {
72+
encodings.push('zstd');
73+
}
74+
5675
t.is(headers.accept, 'application/json');
5776
t.is(headers['user-agent'], 'got (https://github.com/sindresorhus/got)');
58-
t.is(headers['accept-encoding'], supportsBrotli ? 'gzip, deflate, br' : 'gzip, deflate');
77+
t.is(headers['accept-encoding'], encodings.join(', '));
5978
t.is(headers['x-request-id'], 'value');
6079
});
6180

test/zstd.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import {Buffer} from 'node:buffer';
2+
import {promisify} from 'node:util';
3+
import zlib from 'node:zlib';
4+
import test from 'ava';
5+
import getStream from 'get-stream';
6+
import {type HTTPError} from '../source/index.js';
7+
import withServer from './helpers/with-server.js';
8+
9+
const supportsZstd = typeof zlib.zstdCompress === 'function';
10+
11+
if (supportsZstd) {
12+
const testContent = 'Compressible response content.\n';
13+
14+
let zstdData: Buffer;
15+
test.before('setup', async () => {
16+
zstdData = await promisify<string, Buffer>(zlib.zstdCompress)(testContent);
17+
});
18+
19+
test('decompress content', withServer, async (t, server, got) => {
20+
server.get('/', (_request, response) => {
21+
response.setHeader('Content-Encoding', 'zstd');
22+
response.end(zstdData);
23+
});
24+
25+
t.is((await got('')).body, testContent);
26+
});
27+
28+
test('decompress content on error', withServer, async (t, server, got) => {
29+
server.get('/', (_request, response) => {
30+
response.setHeader('Content-Encoding', 'zstd');
31+
response.status(404);
32+
response.end(zstdData);
33+
});
34+
35+
const error = await t.throwsAsync<HTTPError>(got(''));
36+
37+
t.is(error?.response.body, testContent);
38+
});
39+
40+
test('decompress content - stream', withServer, async (t, server, got) => {
41+
server.get('/', (_request, response) => {
42+
response.setHeader('Content-Encoding', 'zstd');
43+
response.end(zstdData);
44+
});
45+
46+
t.is((await getStream(got.stream(''))), testContent);
47+
});
48+
49+
test('handles zstd error', withServer, async (t, server, got) => {
50+
server.get('/', (_request, response) => {
51+
response.setHeader('Content-Encoding', 'zstd');
52+
response.end('Not zstd content');
53+
});
54+
55+
await t.throwsAsync(got(''), {
56+
name: 'ReadError',
57+
});
58+
});
59+
60+
test('handles zstd error - stream', withServer, async (t, server, got) => {
61+
server.get('/', (_request, response) => {
62+
response.setHeader('Content-Encoding', 'zstd');
63+
response.end('Not zstd content');
64+
});
65+
66+
await t.throwsAsync(getStream(got.stream('')), {
67+
name: 'ReadError',
68+
});
69+
});
70+
71+
test('decompress option opts out of decompressing', withServer, async (t, server, got) => {
72+
server.get('/', (_request, response) => {
73+
response.setHeader('Content-Encoding', 'zstd');
74+
response.end(zstdData);
75+
});
76+
77+
const {body} = await got({decompress: false, responseType: 'buffer'});
78+
t.is(Buffer.compare(body, zstdData), 0);
79+
});
80+
81+
test('preserves `headers` property', withServer, async (t, server, got) => {
82+
server.get('/', (_request, response) => {
83+
response.setHeader('Content-Encoding', 'zstd');
84+
response.end(zstdData);
85+
});
86+
87+
t.truthy((await got('')).headers);
88+
});
89+
90+
test('response has `url` and `requestUrl` properties', withServer, async (t, server, got) => {
91+
server.get('/', (_request, response) => {
92+
response.setHeader('Content-Encoding', 'zstd');
93+
response.end(zstdData);
94+
});
95+
96+
const response = await got('');
97+
t.truthy(response.url);
98+
t.truthy(response.requestUrl);
99+
});
100+
} else {
101+
test('zstd support not available - Node.js >= 22.15.0 required', t => {
102+
t.pass('Skipping zstd tests - not supported in this Node.js version');
103+
});
104+
}

0 commit comments

Comments
 (0)