Skip to content

Commit c93ca81

Browse files
committed
Add gzip, deflate, brotli, ztsd & identity encoding endpoints
These are at a different path to httpbin's, partly because it's tidier and partly because they're not fully compatible (they return fixed data, not the full request details in encoded form).
1 parent b8f4f29 commit c93ca81

File tree

9 files changed

+217
-11
lines changed

9 files changed

+217
-11
lines changed

package-lock.json

Lines changed: 11 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"@types/chai": "^4.3.14",
4343
"@types/lodash": "^4.17.0",
4444
"@types/mocha": "^10.0.6",
45-
"@types/node": "^20.11.30",
45+
"@types/node": "^22.15.30",
4646
"@types/node-forge": "^1.3.11",
4747
"chai": "^5.1.0",
4848
"destroyable-server": "^1.0.1",

src/endpoints/http-index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ export * from './http/trailers.js';
3737
export * from './http/error/close.js';
3838
export * from './http/error/reset.js';
3939
export * from './http/example-page.js';
40-
export * from './http/version.js';
40+
export * from './http/version.js';
41+
export * from './http/encoding/gzip.js';
42+
export * from './http/encoding/deflate.js';
43+
export * from './http/encoding/zstd.js';
44+
export * from './http/encoding/brotli.js';
45+
export * from './http/encoding/identity.js';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as zlib from 'node:zlib';
2+
import { serializeJson } from '../../../util.js';
3+
import { HttpEndpoint, HttpHandler } from '../../http-index.js';
4+
5+
// We pre-decode the data. Note that this differs from HTTPBin which returns
6+
// dynamic data here - for this reason, we use a subdirectory. Dynamic encoding
7+
// is relatively expensive so we don't want that. Instead we just use the same
8+
// static data every time, including only a 'brotli' field (which does match
9+
// HTTPBin at least, for that one field).
10+
const data = zlib.brotliCompressSync(serializeJson({
11+
brotli: true
12+
}));
13+
14+
const matchPath = (path: string) => path === '/encoding/brotli';
15+
16+
const handle: HttpHandler = (req, res) => {
17+
res.writeHead(200, {
18+
'content-type': 'application/json',
19+
'content-encoding': 'br'
20+
});
21+
22+
res.end(data);
23+
}
24+
25+
export const brotli: HttpEndpoint = {
26+
matchPath,
27+
handle
28+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as zlib from 'node:zlib';
2+
import { serializeJson } from '../../../util.js';
3+
import { HttpEndpoint, HttpHandler } from '../../http-index.js';
4+
5+
// We pre-decode the data. Note that this differs from HTTPBin which returns
6+
// dynamic data here - for this reason, we use a subdirectory. Dynamic encoding
7+
// is relatively expensive so we don't want that. Instead we just use the same
8+
// static data every time, including only a 'deflate' field.
9+
const data = zlib.deflateSync(serializeJson({
10+
deflate: true
11+
}));
12+
13+
const matchPath = (path: string) => path === '/encoding/deflate';
14+
15+
const handle: HttpHandler = (req, res) => {
16+
res.writeHead(200, {
17+
'content-type': 'application/json',
18+
'content-encoding': 'deflate'
19+
});
20+
21+
res.end(data);
22+
}
23+
24+
export const deflate: HttpEndpoint = {
25+
matchPath,
26+
handle
27+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as zlib from 'node:zlib';
2+
import { serializeJson } from '../../../util.js';
3+
import { HttpEndpoint, HttpHandler } from '../../http-index.js';
4+
5+
// We pre-decode the data. Note that this differs from HTTPBin which returns
6+
// dynamic data here - for this reason, we use a subdirectory. Dynamic encoding
7+
// is relatively expensive so we don't want that. Instead we just use the same
8+
// static data every time, including only a 'gzipped' field (which does match
9+
// HTTPBin at least, for that one field).
10+
const data = zlib.gzipSync(serializeJson({
11+
gzipped: true
12+
}));
13+
14+
const matchPath = (path: string) => path === '/encoding/gzip';
15+
16+
const handle: HttpHandler = (req, res) => {
17+
res.writeHead(200, {
18+
'content-type': 'application/json',
19+
'content-encoding': 'gzip'
20+
});
21+
22+
res.end(data);
23+
}
24+
25+
export const gzip: HttpEndpoint = {
26+
matchPath,
27+
handle
28+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { serializeJson } from '../../../util.js';
2+
import { HttpEndpoint, HttpHandler } from '../../http-index.js';
3+
4+
// We pre-decode the data. Note that this differs from HTTPBin which returns
5+
// dynamic data here - for this reason, we use a subdirectory. Dynamic encoding
6+
// is relatively expensive so we don't want that. Instead we just use the same
7+
// static data every time, including only an 'identity' field.
8+
const data = serializeJson({
9+
identity: true
10+
});
11+
12+
const matchPath = (path: string) => path === '/encoding/identity';
13+
14+
const handle: HttpHandler = (req, res) => {
15+
res.writeHead(200, {
16+
'content-type': 'application/json',
17+
'content-encoding': 'identity'
18+
});
19+
20+
res.end(data);
21+
}
22+
23+
export const identity: HttpEndpoint = {
24+
matchPath,
25+
handle
26+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as zlib from 'node:zlib';
2+
import { serializeJson } from '../../../util.js';
3+
import { HttpEndpoint, HttpHandler } from '../../http-index.js';
4+
5+
// We pre-decode the data. Note that this differs from HTTPBin which returns
6+
// dynamic data here - for this reason, we use a subdirectory. Dynamic encoding
7+
// is relatively expensive so we don't want that. Instead we just use the same
8+
// static data every time, including only a 'zstd' field.
9+
const data = zlib.zstdCompressSync(serializeJson({
10+
zstd: true
11+
}));
12+
13+
const matchPath = (path: string) => path === '/encoding/zstd';
14+
15+
const handle: HttpHandler = (req, res) => {
16+
res.writeHead(200, {
17+
'content-type': 'application/json',
18+
'content-encoding': 'zstd'
19+
});
20+
21+
res.end(data);
22+
}
23+
24+
export const zstd: HttpEndpoint = {
25+
matchPath,
26+
handle
27+
};

test/encodings.spec.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as net from 'node:net';
2+
import * as zlib from 'node:zlib';
3+
import { expect } from 'chai';
4+
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
5+
6+
import { createServer } from '../src/server.js';
7+
8+
describe('Encoding Endpoints', () => {
9+
10+
let server: DestroyableServer;
11+
let serverPort: number;
12+
13+
beforeEach(async () => {
14+
server = makeDestroyable(await createServer());
15+
await new Promise<void>((resolve) => server.listen(resolve));
16+
serverPort = (server.address() as net.AddressInfo).port;
17+
});
18+
19+
afterEach(async () => {
20+
await server.destroy();
21+
});
22+
23+
const testCases = [
24+
{
25+
name: 'gzip',
26+
expectedJson: { gzipped: true }
27+
},
28+
{
29+
name: 'deflate',
30+
expectedJson: { deflate: true }
31+
},
32+
{
33+
name: 'brotli',
34+
encodingName: 'br',
35+
expectedJson: { brotli: true }
36+
},
37+
{
38+
name: 'zstd',
39+
expectedJson: { zstd: true },
40+
decoder: zlib.zstdDecompressSync
41+
},
42+
{
43+
name: 'identity',
44+
expectedJson: { identity: true }
45+
}
46+
];
47+
48+
testCases.forEach(({ name, encodingName, expectedJson, decoder }) => {
49+
it(`/encoding/${name} should return decodeable content`, async () => {
50+
const url = `http://localhost:${serverPort}/encoding/${name}`;
51+
const response = await fetch(url);
52+
53+
expect(response.status).to.equal(200);
54+
expect(response.headers.get('content-encoding')).to.equal(encodingName || name);
55+
56+
const actualJson = decoder
57+
? JSON.parse(decoder(await response.arrayBuffer()).toString('utf8'))
58+
: await response.json(); // Decodeable by fetch automatically
59+
60+
expect(actualJson).to.deep.equals(expectedJson);
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)