Skip to content

Commit 4d02c21

Browse files
committed
Add a trailers endpoint for HTTP trailer testing
1 parent 27f330c commit 4d02c21

File tree

3 files changed

+159
-1
lines changed

3 files changed

+159
-1
lines changed

src/endpoints/http-index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export * from './http/robots.txt.js';
2626
export * from './http/delay.js';
2727
export * from './http/cookies.js'
2828
export * from './http/basic-auth.js';
29-
export * from './http/json.js';
29+
export * from './http/json.js';
30+
export * from './http/trailers.js';

src/endpoints/http/trailers.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { serializeJson } from '../../util.js';
2+
import { HttpEndpoint, HttpHandler } from '../http-index.js';
3+
4+
const matchPath = (path: string) => path === '/trailers';
5+
6+
const handle: HttpHandler = async (req, res) => {
7+
const teHeader = Array.isArray(req.headers['te'])
8+
? req.headers['te'].join(', ')
9+
: req.headers['te'] ?? '';
10+
const willSendTrailers = teHeader
11+
.split(',')
12+
.map(s => s.trim())
13+
.includes('trailers');
14+
15+
if (!req.readableEnded) {
16+
await new Promise((resolve, reject) => {
17+
req.on('end', resolve);
18+
req.on('error', reject);
19+
req.resume();
20+
});
21+
}
22+
23+
const rawTrailers = req.rawTrailers;
24+
25+
res.writeHead(200, {
26+
'trailer': 'example-trailer',
27+
'content-type': 'application/json'
28+
});
29+
30+
if (willSendTrailers) {
31+
res.addTrailers({
32+
'example-trailer': 'example value'
33+
});
34+
}
35+
36+
res.end(serializeJson({
37+
"received-trailers": rawTrailers,
38+
"will-send-trailers": willSendTrailers
39+
}));
40+
}
41+
42+
export const trailers: HttpEndpoint = {
43+
matchPath,
44+
handle
45+
};

test/trailers.spec.ts

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as net from 'net';
2+
import * as http from 'http';
3+
import { expect } from 'chai';
4+
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
5+
import * as streamConsumers from 'stream/consumers';
6+
7+
import { createServer } from '../src/server.js';
8+
import { delay } from '@httptoolkit/util';
9+
10+
describe("Trailers endpoint", () => {
11+
12+
let server: DestroyableServer;
13+
let serverPort: number;
14+
15+
beforeEach(async () => {
16+
server = makeDestroyable(await createServer());
17+
await new Promise<void>((resolve) => server.listen(resolve));
18+
serverPort = (server.address() as net.AddressInfo).port;
19+
});
20+
21+
afterEach(async () => {
22+
await server.destroy();
23+
});
24+
25+
it("sends no trailers or metadata for a plain request", async () => {
26+
const address = `http://localhost:${serverPort}/trailers`;
27+
const response = await fetch(address);
28+
29+
expect(response.status).to.equal(200);
30+
31+
const body = await response.json();
32+
expect(body).to.deep.equal({
33+
'will-send-trailers': false,
34+
'received-trailers': []
35+
});
36+
});
37+
38+
it("sends no trailers given a plain request", async () => {
39+
const address = `http://localhost:${serverPort}/trailers`;
40+
const request = http.request(address).end();
41+
42+
const response: http.IncomingMessage = await new Promise((resolve, reject) => {
43+
request.on('response', resolve);
44+
request.on('error', reject);
45+
});
46+
47+
const responseData = await streamConsumers.json(response);
48+
49+
expect(response.statusCode).to.equal(200);
50+
expect(responseData).to.deep.equal({
51+
'will-send-trailers': false,
52+
'received-trailers': []
53+
});
54+
55+
expect(response.rawTrailers).to.deep.equal([]);
56+
});
57+
58+
it("sends trailers given TE: trailers", async () => {
59+
const address = `http://localhost:${serverPort}/trailers`;
60+
const request = http.request(address, {
61+
headers: {
62+
'TE': 'trailers'
63+
}
64+
}).end();
65+
66+
const response: http.IncomingMessage = await new Promise((resolve, reject) => {
67+
request.on('response', resolve);
68+
request.on('error', reject);
69+
});
70+
71+
const responseData = await streamConsumers.json(response);
72+
73+
expect(response.statusCode).to.equal(200);
74+
expect(responseData).to.deep.equal({
75+
'will-send-trailers': true,
76+
'received-trailers': []
77+
});
78+
79+
expect(response.rawTrailers).to.deep.equal([
80+
'example-trailer', 'example value'
81+
]);
82+
});
83+
84+
it("logs the received trailers if provided", async () => {
85+
const address = `http://localhost:${serverPort}/trailers`;
86+
const request = http.request(address, {
87+
method: 'POST'
88+
});
89+
90+
request.flushHeaders();
91+
await delay(50); // Make sure it handles slow requests
92+
93+
request.addTrailers([
94+
['request-TRAILER', 'Request value'],
95+
]);
96+
request.end('hello');
97+
98+
const response: http.IncomingMessage = await new Promise((resolve, reject) => {
99+
request.on('response', resolve);
100+
request.on('error', reject);
101+
});
102+
103+
const responseData = await streamConsumers.json(response);
104+
105+
expect(response.statusCode).to.equal(200);
106+
expect(responseData).to.deep.equal({
107+
'will-send-trailers': false,
108+
'received-trailers': ['request-TRAILER', 'Request value']
109+
});
110+
});
111+
112+
});

0 commit comments

Comments
 (0)