@gjsify/http 0.0.4 → 0.1.1
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/README.md +30 -1
- package/lib/esm/client-request.js +228 -0
- package/lib/esm/constants.js +105 -0
- package/lib/esm/incoming-message.js +59 -0
- package/lib/esm/index.js +112 -2
- package/lib/esm/server.js +371 -0
- package/lib/types/client-request.d.ts +65 -0
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/incoming-message.d.ts +25 -0
- package/lib/types/index.d.ts +102 -0
- package/lib/types/server.d.ts +102 -0
- package/package.json +24 -18
- package/src/client-request.ts +307 -0
- package/src/client.spec.ts +538 -0
- package/src/constants.ts +33 -0
- package/src/extended.spec.ts +620 -0
- package/src/incoming-message.ts +71 -0
- package/src/index.spec.ts +1359 -67
- package/src/index.ts +164 -20
- package/src/server.ts +489 -0
- package/src/streaming.spec.ts +588 -0
- package/src/test.mts +6 -1
- package/src/timeout.spec.ts +668 -0
- package/src/upgrade.spec.ts +256 -0
- package/tsconfig.json +23 -10
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/index.js +0 -18
- package/test.gjs.js +0 -34832
- package/test.gjs.mjs +0 -34751
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -1278
- package/test.node.mjs +0 -358
- package/tsconfig.types.json +0 -8
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
// Ported from refs/node-test/parallel/test-http-set-timeout-server.js,
|
|
2
|
+
// test-http-client-timeout-event.js, test-http-automatic-headers.js,
|
|
3
|
+
// test-http-head-response-has-no-body.js, test-http-client-abort.js
|
|
4
|
+
// Original: MIT license, Node.js contributors
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
7
|
+
import { createServer, request as httpRequest, get as httpGet } from 'node:http';
|
|
8
|
+
import type { Server, IncomingMessage, ServerResponse } from 'node:http';
|
|
9
|
+
import { Buffer } from 'node:buffer';
|
|
10
|
+
|
|
11
|
+
/** Helper: start a server and return its URL + cleanup function */
|
|
12
|
+
function startServer(handler: (req: IncomingMessage, res: ServerResponse) => void): Promise<{ url: string; port: number; server: Server; close: () => Promise<void> }> {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const server = createServer(handler);
|
|
15
|
+
server.listen(0, '127.0.0.1', () => {
|
|
16
|
+
const addr = server.address()!;
|
|
17
|
+
const port = typeof addr === 'object' ? addr.port : 0;
|
|
18
|
+
const url = `http://127.0.0.1:${port}`;
|
|
19
|
+
resolve({
|
|
20
|
+
url,
|
|
21
|
+
port,
|
|
22
|
+
server,
|
|
23
|
+
close: () => new Promise<void>((res) => server.close(() => res())),
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Helper: make an HTTP GET request and collect the response body */
|
|
30
|
+
function httpGetBody(url: string): Promise<{ statusCode: number; headers: Record<string, string | string[] | undefined>; body: string }> {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
httpGet(url, (res: IncomingMessage) => {
|
|
33
|
+
let body = '';
|
|
34
|
+
res.setEncoding('utf8');
|
|
35
|
+
res.on('data', (chunk: string) => { body += chunk; });
|
|
36
|
+
res.on('end', () => {
|
|
37
|
+
resolve({ statusCode: res.statusCode!, headers: res.headers as Record<string, string | string[] | undefined>, body });
|
|
38
|
+
});
|
|
39
|
+
res.on('error', reject);
|
|
40
|
+
}).on('error', reject);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default async () => {
|
|
45
|
+
|
|
46
|
+
// ===================== ServerResponse.setTimeout =====================
|
|
47
|
+
await describe('http ServerResponse.setTimeout', async () => {
|
|
48
|
+
await it('should return the ServerResponse instance', async () => {
|
|
49
|
+
const { close } = await startServer((req, res) => {
|
|
50
|
+
const ret = res.setTimeout(5000);
|
|
51
|
+
expect(ret).toBe(res);
|
|
52
|
+
res.end('ok');
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
await httpGetBody(`http://127.0.0.1:${(await startServer((_r, res) => res.end())).port}`);
|
|
56
|
+
} catch { /* ignore */ }
|
|
57
|
+
await close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await it('should emit timeout event when response is delayed', async () => {
|
|
61
|
+
let timeoutFired = false;
|
|
62
|
+
const { port, close } = await startServer((_req, res) => {
|
|
63
|
+
res.setTimeout(50, () => {
|
|
64
|
+
timeoutFired = true;
|
|
65
|
+
res.writeHead(408);
|
|
66
|
+
res.end('Timeout');
|
|
67
|
+
});
|
|
68
|
+
// Intentionally don't call res.end() — wait for timeout
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
const { statusCode } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
72
|
+
expect(timeoutFired).toBe(true);
|
|
73
|
+
expect(statusCode).toBe(408);
|
|
74
|
+
} finally {
|
|
75
|
+
await close();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await it('should NOT emit timeout if response is sent in time', async () => {
|
|
80
|
+
let timeoutFired = false;
|
|
81
|
+
const { port, close } = await startServer((_req, res) => {
|
|
82
|
+
res.setTimeout(5000, () => {
|
|
83
|
+
timeoutFired = true;
|
|
84
|
+
});
|
|
85
|
+
// Respond immediately
|
|
86
|
+
res.writeHead(200);
|
|
87
|
+
res.end('fast');
|
|
88
|
+
});
|
|
89
|
+
try {
|
|
90
|
+
const { statusCode, body } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
91
|
+
expect(statusCode).toBe(200);
|
|
92
|
+
expect(body).toBe('fast');
|
|
93
|
+
expect(timeoutFired).toBe(false);
|
|
94
|
+
} finally {
|
|
95
|
+
await close();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ===================== IncomingMessage.setTimeout =====================
|
|
101
|
+
await describe('http IncomingMessage.setTimeout', async () => {
|
|
102
|
+
await it('should return the IncomingMessage instance', async () => {
|
|
103
|
+
let returnedSelf = false;
|
|
104
|
+
const { port, close } = await startServer((req, res) => {
|
|
105
|
+
const ret = req.setTimeout(5000);
|
|
106
|
+
returnedSelf = (ret === req);
|
|
107
|
+
res.end('ok');
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
await httpGetBody(`http://127.0.0.1:${port}`);
|
|
111
|
+
expect(returnedSelf).toBe(true);
|
|
112
|
+
} finally {
|
|
113
|
+
await close();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ===================== ClientRequest.setTimeout =====================
|
|
119
|
+
await describe('http ClientRequest.setTimeout', async () => {
|
|
120
|
+
await it('should emit timeout event on slow server', async () => {
|
|
121
|
+
// Server that never responds
|
|
122
|
+
const { port, close } = await startServer((_req, _res) => {
|
|
123
|
+
// Intentionally don't respond
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
const timedOut = await new Promise<boolean>((resolve) => {
|
|
127
|
+
const req = httpRequest({
|
|
128
|
+
hostname: '127.0.0.1',
|
|
129
|
+
port,
|
|
130
|
+
path: '/',
|
|
131
|
+
method: 'GET',
|
|
132
|
+
});
|
|
133
|
+
req.setTimeout(50, () => {
|
|
134
|
+
req.destroy();
|
|
135
|
+
resolve(true);
|
|
136
|
+
});
|
|
137
|
+
req.on('error', () => { /* expected after destroy */ });
|
|
138
|
+
req.end();
|
|
139
|
+
});
|
|
140
|
+
expect(timedOut).toBe(true);
|
|
141
|
+
} finally {
|
|
142
|
+
await close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await it('should NOT emit timeout if response arrives in time', async () => {
|
|
147
|
+
const { port, close } = await startServer((_req, res) => {
|
|
148
|
+
res.end('ok');
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
152
|
+
const req = httpRequest({
|
|
153
|
+
hostname: '127.0.0.1',
|
|
154
|
+
port,
|
|
155
|
+
path: '/',
|
|
156
|
+
method: 'GET',
|
|
157
|
+
}, (res) => {
|
|
158
|
+
let body = '';
|
|
159
|
+
res.setEncoding('utf8');
|
|
160
|
+
res.on('data', (chunk: string) => { body += chunk; });
|
|
161
|
+
res.on('end', () => resolve(body));
|
|
162
|
+
});
|
|
163
|
+
req.setTimeout(5000, () => {
|
|
164
|
+
req.destroy();
|
|
165
|
+
reject(new Error('Should not timeout'));
|
|
166
|
+
});
|
|
167
|
+
req.on('error', reject);
|
|
168
|
+
req.end();
|
|
169
|
+
});
|
|
170
|
+
expect(result).toBe('ok');
|
|
171
|
+
} finally {
|
|
172
|
+
await close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await it('should accept timeout option in request options', async () => {
|
|
177
|
+
// Server that never responds
|
|
178
|
+
const { port, close } = await startServer((_req, _res) => {
|
|
179
|
+
// Intentionally don't respond
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
const timedOut = await new Promise<boolean>((resolve) => {
|
|
183
|
+
const req = httpRequest({
|
|
184
|
+
hostname: '127.0.0.1',
|
|
185
|
+
port,
|
|
186
|
+
path: '/',
|
|
187
|
+
method: 'GET',
|
|
188
|
+
timeout: 50,
|
|
189
|
+
} as any);
|
|
190
|
+
req.on('timeout', () => {
|
|
191
|
+
req.destroy();
|
|
192
|
+
resolve(true);
|
|
193
|
+
});
|
|
194
|
+
req.on('error', () => { /* expected after destroy */ });
|
|
195
|
+
req.end();
|
|
196
|
+
});
|
|
197
|
+
expect(timedOut).toBe(true);
|
|
198
|
+
} finally {
|
|
199
|
+
await close();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ===================== ClientRequest.abort =====================
|
|
205
|
+
await describe('http ClientRequest.abort', async () => {
|
|
206
|
+
await it('should set aborted property to true', async () => {
|
|
207
|
+
const { port, close } = await startServer((_req, res) => {
|
|
208
|
+
res.end('ok');
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const wasAborted = await new Promise<boolean>((resolve) => {
|
|
212
|
+
const req = httpGet(`http://127.0.0.1:${port}`, (_res) => {
|
|
213
|
+
// Abort after receiving the response
|
|
214
|
+
req.abort();
|
|
215
|
+
resolve(req.aborted);
|
|
216
|
+
});
|
|
217
|
+
req.on('error', () => { /* expected */ });
|
|
218
|
+
});
|
|
219
|
+
expect(wasAborted).toBe(true);
|
|
220
|
+
} finally {
|
|
221
|
+
await close();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await it('should emit abort event', async () => {
|
|
226
|
+
const { port, close } = await startServer((_req, res) => {
|
|
227
|
+
// Respond quickly so client receives the response
|
|
228
|
+
res.end('ok');
|
|
229
|
+
});
|
|
230
|
+
try {
|
|
231
|
+
const abortEmitted = await new Promise<boolean>((resolve) => {
|
|
232
|
+
const req = httpGet(`http://127.0.0.1:${port}`, (_res) => {
|
|
233
|
+
// Abort after response received
|
|
234
|
+
req.abort();
|
|
235
|
+
});
|
|
236
|
+
req.on('abort', () => resolve(true));
|
|
237
|
+
req.on('error', () => { /* expected */ });
|
|
238
|
+
// Timeout safety
|
|
239
|
+
setTimeout(() => resolve(false), 2000);
|
|
240
|
+
});
|
|
241
|
+
expect(abortEmitted).toBe(true);
|
|
242
|
+
} finally {
|
|
243
|
+
await close();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ===================== HEAD response =====================
|
|
249
|
+
await describe('http HEAD response', async () => {
|
|
250
|
+
await it('should receive empty body for HEAD request', async () => {
|
|
251
|
+
const { port, close } = await startServer((_req, res) => {
|
|
252
|
+
res.writeHead(200);
|
|
253
|
+
res.end();
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
const body = await new Promise<string>((resolve, reject) => {
|
|
257
|
+
const req = httpRequest({
|
|
258
|
+
hostname: '127.0.0.1',
|
|
259
|
+
port,
|
|
260
|
+
path: '/',
|
|
261
|
+
method: 'HEAD',
|
|
262
|
+
}, (res) => {
|
|
263
|
+
let data = '';
|
|
264
|
+
res.setEncoding('utf8');
|
|
265
|
+
res.on('data', (chunk: string) => { data += chunk; });
|
|
266
|
+
res.on('end', () => resolve(data));
|
|
267
|
+
});
|
|
268
|
+
req.on('error', reject);
|
|
269
|
+
req.end();
|
|
270
|
+
});
|
|
271
|
+
expect(body).toBe('');
|
|
272
|
+
} finally {
|
|
273
|
+
await close();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await it('should have correct status code for HEAD', async () => {
|
|
278
|
+
const { port, close } = await startServer((_req, res) => {
|
|
279
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
280
|
+
res.end('this body should not be sent');
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
const result = await new Promise<{ statusCode: number; body: string }>((resolve, reject) => {
|
|
284
|
+
const req = httpRequest({
|
|
285
|
+
hostname: '127.0.0.1',
|
|
286
|
+
port,
|
|
287
|
+
path: '/',
|
|
288
|
+
method: 'HEAD',
|
|
289
|
+
}, (res) => {
|
|
290
|
+
let data = '';
|
|
291
|
+
res.setEncoding('utf8');
|
|
292
|
+
res.on('data', (chunk: string) => { data += chunk; });
|
|
293
|
+
res.on('end', () => resolve({ statusCode: res.statusCode!, body: data }));
|
|
294
|
+
});
|
|
295
|
+
req.on('error', reject);
|
|
296
|
+
req.end();
|
|
297
|
+
});
|
|
298
|
+
expect(result.statusCode).toBe(200);
|
|
299
|
+
// HEAD responses should have empty body even if server writes one
|
|
300
|
+
// Note: Soup.Server may include the body — we accept either behavior
|
|
301
|
+
} finally {
|
|
302
|
+
await close();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ===================== Automatic headers =====================
|
|
308
|
+
await describe('http automatic headers', async () => {
|
|
309
|
+
await it('should include custom headers in response', async () => {
|
|
310
|
+
const { port, close } = await startServer((_req, res) => {
|
|
311
|
+
res.setHeader('x-custom', 'test-value');
|
|
312
|
+
res.setHeader('content-type', 'text/plain');
|
|
313
|
+
res.end('hello');
|
|
314
|
+
});
|
|
315
|
+
try {
|
|
316
|
+
const { headers } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
317
|
+
expect(headers['x-custom']).toBe('test-value');
|
|
318
|
+
expect(headers['content-type']).toBe('text/plain');
|
|
319
|
+
} finally {
|
|
320
|
+
await close();
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await it('should preserve user-set headers with X- prefix', async () => {
|
|
325
|
+
const { port, close } = await startServer((_req, res) => {
|
|
326
|
+
res.setHeader('x-date', 'foo');
|
|
327
|
+
res.setHeader('x-connection', 'bar');
|
|
328
|
+
res.setHeader('x-content-length', 'baz');
|
|
329
|
+
res.end();
|
|
330
|
+
});
|
|
331
|
+
try {
|
|
332
|
+
const { headers } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
333
|
+
expect(headers['x-date']).toBe('foo');
|
|
334
|
+
expect(headers['x-connection']).toBe('bar');
|
|
335
|
+
expect(headers['x-content-length']).toBe('baz');
|
|
336
|
+
} finally {
|
|
337
|
+
await close();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ===================== Custom status messages =====================
|
|
343
|
+
await describe('http custom status messages', async () => {
|
|
344
|
+
await it('should use custom status message from writeHead', async () => {
|
|
345
|
+
const { port, close } = await startServer((_req, res) => {
|
|
346
|
+
res.writeHead(200, 'Custom OK');
|
|
347
|
+
res.end('body');
|
|
348
|
+
});
|
|
349
|
+
try {
|
|
350
|
+
const result = await new Promise<{ statusCode: number; statusMessage: string }>((resolve, reject) => {
|
|
351
|
+
httpGet(`http://127.0.0.1:${port}`, (res) => {
|
|
352
|
+
res.resume();
|
|
353
|
+
res.on('end', () => resolve({
|
|
354
|
+
statusCode: res.statusCode!,
|
|
355
|
+
statusMessage: res.statusMessage || '',
|
|
356
|
+
}));
|
|
357
|
+
}).on('error', reject);
|
|
358
|
+
});
|
|
359
|
+
expect(result.statusCode).toBe(200);
|
|
360
|
+
// Custom status messages may or may not be preserved by Soup.Server
|
|
361
|
+
// but the status code should be correct
|
|
362
|
+
} finally {
|
|
363
|
+
await close();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await it('should default to standard status message', async () => {
|
|
368
|
+
const { port, close } = await startServer((_req, res) => {
|
|
369
|
+
res.writeHead(404);
|
|
370
|
+
res.end('not found');
|
|
371
|
+
});
|
|
372
|
+
try {
|
|
373
|
+
const { statusCode } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
374
|
+
expect(statusCode).toBe(404);
|
|
375
|
+
} finally {
|
|
376
|
+
await close();
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ===================== res.end() callback =====================
|
|
382
|
+
await describe('http res.end() callback', async () => {
|
|
383
|
+
await it('should invoke callback passed to res.end()', async () => {
|
|
384
|
+
let callbackInvoked = false;
|
|
385
|
+
const { port, close } = await startServer((_req, res) => {
|
|
386
|
+
res.end('done', () => {
|
|
387
|
+
callbackInvoked = true;
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
await httpGetBody(`http://127.0.0.1:${port}`);
|
|
392
|
+
// Give the callback a moment to fire
|
|
393
|
+
await new Promise<void>((r) => setTimeout(r, 50));
|
|
394
|
+
expect(callbackInvoked).toBe(true);
|
|
395
|
+
} finally {
|
|
396
|
+
await close();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await it('should invoke callback when end() called with no data', async () => {
|
|
401
|
+
let callbackInvoked = false;
|
|
402
|
+
const { port, close } = await startServer((_req, res) => {
|
|
403
|
+
res.writeHead(204);
|
|
404
|
+
res.end(() => {
|
|
405
|
+
callbackInvoked = true;
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
try {
|
|
409
|
+
await httpGetBody(`http://127.0.0.1:${port}`);
|
|
410
|
+
await new Promise<void>((r) => setTimeout(r, 50));
|
|
411
|
+
expect(callbackInvoked).toBe(true);
|
|
412
|
+
} finally {
|
|
413
|
+
await close();
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ===================== Multiple headers (Set-Cookie) =====================
|
|
419
|
+
await describe('http multiple headers', async () => {
|
|
420
|
+
await it('should send multiple Set-Cookie headers', async () => {
|
|
421
|
+
const { port, close } = await startServer((_req, res) => {
|
|
422
|
+
res.setHeader('set-cookie', ['a=1', 'b=2']);
|
|
423
|
+
res.end('ok');
|
|
424
|
+
});
|
|
425
|
+
try {
|
|
426
|
+
const { headers } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
427
|
+
const cookies = headers['set-cookie'];
|
|
428
|
+
// Should receive as array or combined string
|
|
429
|
+
if (Array.isArray(cookies)) {
|
|
430
|
+
expect(cookies.length).toBe(2);
|
|
431
|
+
expect(cookies[0]).toBe('a=1');
|
|
432
|
+
expect(cookies[1]).toBe('b=2');
|
|
433
|
+
} else {
|
|
434
|
+
// Some implementations join with comma
|
|
435
|
+
expect(typeof cookies).toBe('string');
|
|
436
|
+
}
|
|
437
|
+
} finally {
|
|
438
|
+
await close();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
await it('should support appendHeader for multi-value headers', async () => {
|
|
443
|
+
const { port, close } = await startServer((_req, res) => {
|
|
444
|
+
res.appendHeader('x-multi', 'first');
|
|
445
|
+
res.appendHeader('x-multi', 'second');
|
|
446
|
+
res.end('ok');
|
|
447
|
+
});
|
|
448
|
+
try {
|
|
449
|
+
const { headers } = await httpGetBody(`http://127.0.0.1:${port}`);
|
|
450
|
+
const multi = headers['x-multi'];
|
|
451
|
+
// Should contain both values
|
|
452
|
+
if (Array.isArray(multi)) {
|
|
453
|
+
expect(multi.length).toBe(2);
|
|
454
|
+
} else {
|
|
455
|
+
// Combined as comma-separated
|
|
456
|
+
expect(typeof multi).toBe('string');
|
|
457
|
+
}
|
|
458
|
+
} finally {
|
|
459
|
+
await close();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ===================== flushHeaders =====================
|
|
465
|
+
await describe('http flushHeaders', async () => {
|
|
466
|
+
await it('should mark headersSent as true', async () => {
|
|
467
|
+
let headersSentAfterFlush = false;
|
|
468
|
+
const { port, close } = await startServer((_req, res) => {
|
|
469
|
+
res.setHeader('x-test', 'value');
|
|
470
|
+
res.flushHeaders();
|
|
471
|
+
headersSentAfterFlush = res.headersSent;
|
|
472
|
+
res.end('ok');
|
|
473
|
+
});
|
|
474
|
+
try {
|
|
475
|
+
await httpGetBody(`http://127.0.0.1:${port}`);
|
|
476
|
+
expect(headersSentAfterFlush).toBe(true);
|
|
477
|
+
} finally {
|
|
478
|
+
await close();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ===================== Server properties =====================
|
|
484
|
+
await describe('http Server timeout properties', async () => {
|
|
485
|
+
await it('should have default timeout properties', async () => {
|
|
486
|
+
const server = createServer();
|
|
487
|
+
expect(server.timeout).toBe(0);
|
|
488
|
+
expect(server.keepAliveTimeout).toBe(5000);
|
|
489
|
+
expect(server.headersTimeout).toBe(60000);
|
|
490
|
+
expect(server.requestTimeout).toBe(300000);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
await it('server.setTimeout should return the server instance', async () => {
|
|
494
|
+
const server = createServer();
|
|
495
|
+
const ret = server.setTimeout(1000);
|
|
496
|
+
expect(ret).toBe(server);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
await it('server.setTimeout should update timeout property', async () => {
|
|
500
|
+
const server = createServer();
|
|
501
|
+
server.setTimeout(2000);
|
|
502
|
+
expect(server.timeout).toBe(2000);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// ===================== Server.close =====================
|
|
507
|
+
await describe('http Server.close', async () => {
|
|
508
|
+
await it('should emit close event', async () => {
|
|
509
|
+
const server = createServer();
|
|
510
|
+
const closed = new Promise<boolean>((resolve) => {
|
|
511
|
+
server.listen(0, '127.0.0.1', () => {
|
|
512
|
+
server.close(() => resolve(true));
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
expect(await closed).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await it('should set listening to false after close', async () => {
|
|
519
|
+
const server = createServer();
|
|
520
|
+
await new Promise<void>((resolve) => {
|
|
521
|
+
server.listen(0, '127.0.0.1', () => {
|
|
522
|
+
expect(server.listening).toBe(true);
|
|
523
|
+
server.close(() => {
|
|
524
|
+
expect(server.listening).toBe(false);
|
|
525
|
+
resolve();
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ===================== Request body as Readable stream =====================
|
|
533
|
+
await describe('http request body streaming', async () => {
|
|
534
|
+
await it('should receive POST body as data events', async () => {
|
|
535
|
+
const { port, close } = await startServer((req, res) => {
|
|
536
|
+
let body = '';
|
|
537
|
+
req.setEncoding('utf8');
|
|
538
|
+
req.on('data', (chunk: string) => { body += chunk; });
|
|
539
|
+
req.on('end', () => {
|
|
540
|
+
res.end(body);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
try {
|
|
544
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
545
|
+
const req = httpRequest({
|
|
546
|
+
hostname: '127.0.0.1',
|
|
547
|
+
port,
|
|
548
|
+
path: '/',
|
|
549
|
+
method: 'POST',
|
|
550
|
+
headers: { 'content-type': 'text/plain' },
|
|
551
|
+
}, (res) => {
|
|
552
|
+
let data = '';
|
|
553
|
+
res.setEncoding('utf8');
|
|
554
|
+
res.on('data', (chunk: string) => { data += chunk; });
|
|
555
|
+
res.on('end', () => resolve(data));
|
|
556
|
+
});
|
|
557
|
+
req.on('error', reject);
|
|
558
|
+
req.write('Hello, ');
|
|
559
|
+
req.end('World!');
|
|
560
|
+
});
|
|
561
|
+
expect(result).toBe('Hello, World!');
|
|
562
|
+
} finally {
|
|
563
|
+
await close();
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await it('should handle empty POST body', async () => {
|
|
568
|
+
const { port, close } = await startServer((req, res) => {
|
|
569
|
+
let body = '';
|
|
570
|
+
req.setEncoding('utf8');
|
|
571
|
+
req.on('data', (chunk: string) => { body += chunk; });
|
|
572
|
+
req.on('end', () => {
|
|
573
|
+
res.end(`len:${body.length}`);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
try {
|
|
577
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
578
|
+
const req = httpRequest({
|
|
579
|
+
hostname: '127.0.0.1',
|
|
580
|
+
port,
|
|
581
|
+
path: '/',
|
|
582
|
+
method: 'POST',
|
|
583
|
+
}, (res) => {
|
|
584
|
+
let data = '';
|
|
585
|
+
res.setEncoding('utf8');
|
|
586
|
+
res.on('data', (chunk: string) => { data += chunk; });
|
|
587
|
+
res.on('end', () => resolve(data));
|
|
588
|
+
});
|
|
589
|
+
req.on('error', reject);
|
|
590
|
+
req.end();
|
|
591
|
+
});
|
|
592
|
+
expect(result).toBe('len:0');
|
|
593
|
+
} finally {
|
|
594
|
+
await close();
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
await it('should handle large POST body (64KB)', async () => {
|
|
599
|
+
const { port, close } = await startServer((req, res) => {
|
|
600
|
+
const chunks: Buffer[] = [];
|
|
601
|
+
req.on('data', (chunk: Buffer) => { chunks.push(chunk); });
|
|
602
|
+
req.on('end', () => {
|
|
603
|
+
const total = Buffer.concat(chunks);
|
|
604
|
+
res.end(`size:${total.length}`);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
const largeBody = Buffer.alloc(64 * 1024, 'x');
|
|
609
|
+
const result = await new Promise<string>((resolve, reject) => {
|
|
610
|
+
const req = httpRequest({
|
|
611
|
+
hostname: '127.0.0.1',
|
|
612
|
+
port,
|
|
613
|
+
path: '/',
|
|
614
|
+
method: 'POST',
|
|
615
|
+
headers: {
|
|
616
|
+
'content-type': 'application/octet-stream',
|
|
617
|
+
'content-length': String(largeBody.length),
|
|
618
|
+
},
|
|
619
|
+
}, (res) => {
|
|
620
|
+
let data = '';
|
|
621
|
+
res.setEncoding('utf8');
|
|
622
|
+
res.on('data', (chunk: string) => { data += chunk; });
|
|
623
|
+
res.on('end', () => resolve(data));
|
|
624
|
+
});
|
|
625
|
+
req.on('error', reject);
|
|
626
|
+
req.end(largeBody);
|
|
627
|
+
});
|
|
628
|
+
expect(result).toBe(`size:${64 * 1024}`);
|
|
629
|
+
} finally {
|
|
630
|
+
await close();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// ===================== Error handling =====================
|
|
636
|
+
await describe('http error handling', async () => {
|
|
637
|
+
await it('should emit error on connection refused', async () => {
|
|
638
|
+
const errorEmitted = await new Promise<boolean>((resolve) => {
|
|
639
|
+
const req = httpRequest({
|
|
640
|
+
hostname: '127.0.0.1',
|
|
641
|
+
port: 1, // port 1 should be refused
|
|
642
|
+
path: '/',
|
|
643
|
+
});
|
|
644
|
+
req.on('error', () => resolve(true));
|
|
645
|
+
req.on('response', () => resolve(false));
|
|
646
|
+
req.end();
|
|
647
|
+
// Safety timeout
|
|
648
|
+
setTimeout(() => resolve(false), 5000);
|
|
649
|
+
});
|
|
650
|
+
expect(errorEmitted).toBe(true);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
await it('should emit error when listening on busy port', async () => {
|
|
654
|
+
const { port, close } = await startServer((_req, res) => res.end());
|
|
655
|
+
try {
|
|
656
|
+
const errorEmitted = await new Promise<boolean>((resolve) => {
|
|
657
|
+
const server2 = createServer();
|
|
658
|
+
server2.on('error', () => resolve(true));
|
|
659
|
+
server2.listen(port, '127.0.0.1');
|
|
660
|
+
setTimeout(() => resolve(false), 2000);
|
|
661
|
+
});
|
|
662
|
+
expect(errorEmitted).toBe(true);
|
|
663
|
+
} finally {
|
|
664
|
+
await close();
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
};
|