Skip to content
File

Blob: src/workerd/api/node/tests/http-client-nodejs-test.js

javascript644 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import http from 'node:http';
6import { strictEqual, ok, throws, deepStrictEqual } from 'node:assert';
7import { once } from 'node:events';
8import { get } from 'node:http';
9import { gunzipSync } from 'node:zlib';
10 
11export const checkPortsSetCorrectly = {
12 test(_ctrl, env) {
13 const keys = [
14 'SIDECAR_HOSTNAME',
15 'PONG_SERVER_PORT',
16 'ASD_SERVER_PORT',
17 'DEFAULT_HEADERS_EXIST_PORT',
18 'REQUEST_ARGUMENTS_PORT',
19 'HELLO_WORLD_SERVER_PORT',
20 'GZIP_SERVER_PORT',
21 ];
22 for (const key of keys) {
23 strictEqual(typeof env[key], 'string');
24 ok(env[key].length > 0);
25 }
26 },
27};
28 
29// TODO(soon): This is triggering io-context error. Fix this edge case.
30// Test is taken from Node.js: test/parallel/test-http-client-request-options.js
31// export const testHttpClientRequestOptions = {
32// async test(_ctrl, env) {
33// const headers = { foo: 'Bar' };
34// const { promise, resolve, reject } = Promise.withResolvers();
35// const url = new URL(`http://localhost:${env.PONG_SERVER_PORT}/ping?q=term`);
36// url.headers = headers;
37// const clientReq = http.request(url);
38// clientReq.on('close', resolve);
39// clientReq.on('error', reject)
40// clientReq.end();
41// await promise;
42// },
43// };
44 
45// Test is taken from test/parallel/test-http-client-res-destroyed.js
46export const testHttpClientResDestroyed = {
47 async test(_ctrl, env) {
48 {
49 const { promise, resolve } = Promise.withResolvers();
50 http.get(
51 {
52 hostname: env.SIDECAR_HOSTNAME,
53 port: env.ASD_SERVER_PORT,
54 },
55 (res) => {
56 strictEqual(res.destroyed, false);
57 res.destroy();
58 strictEqual(res.destroyed, true);
59 res.on('close', resolve);
60 }
61 );
62 await promise;
63 }
64 
65 {
66 const { promise, resolve } = Promise.withResolvers();
67 http.get(
68 {
69 hostname: env.SIDECAR_HOSTNAME,
70 port: env.ASD_SERVER_PORT,
71 },
72 (res) => {
73 strictEqual(res.destroyed, false);
74 res
75 .on('close', () => {
76 strictEqual(res.destroyed, true);
77 resolve();
78 })
79 .resume();
80 }
81 );
82 await promise;
83 }
84 },
85};
86 
87// TODO(soon): Support this test case, if possible with the current implementation
88// Test is taken from test/parallel/test-http-client-response-timeout.js
89// export const testHttpClientResponseTimeout = {
90// async test(_ctrl, env) {
91// const { promise, resolve } = Promise.withResolvers();
92// const req =
93// http.get({ port: env.TIMEOUT_SERVER_PORT }, (res) => {
94// res.on('timeout', () => {
95// resolve();
96// req.destroy();
97// });
98// res.setTimeout(1);
99// });
100// await promise;
101// },
102// };
103 
104// TODO(soon): Handle this edge case.
105// Test is taken from test/parallel/test-http-client-close-event.js
106// export const testHttpClientCloseEvent = {
107// async test(_ctrl, env) {
108// const { promise, resolve, reject } = Promise.withResolvers();
109// const req = http.get({ port: env.PONG_SERVER_PORT }, () => {
110// reject(new Error('Should not have called this callback'));
111// });
112 
113// const errFn = mock.fn((err) => {
114// strictEqual(err.constructor, Error);
115// strictEqual(err.message, 'socket hang up');
116// strictEqual(err.code, 'ECONNRESET');
117// });
118// req.on('error', errFn);
119 
120// req.on('close', () => {
121// strictEqual(req.destroyed, true);
122// strictEqual(errFn.mock.callCount(), 1);
123// resolve();
124// });
125 
126// req.destroy();
127// await promise;
128// },
129// };
130 
131// Test is taken from test/parallel/test-http-client-default-headers-exist.js
132export const testHttpClientDefaultHeadersExist = {
133 async test(_ctrl, env) {
134 const expectedHeaders = {
135 DELETE: ['host', 'connection'],
136 GET: ['host', 'connection'],
137 HEAD: ['host', 'connection'],
138 OPTIONS: ['host', 'connection'],
139 POST: ['host', 'connection', 'content-length'],
140 PUT: ['host', 'connection', 'content-length'],
141 TRACE: ['host', 'connection'],
142 };
143 
144 const expectedMethods = Object.keys(expectedHeaders);
145 
146 await Promise.all(
147 expectedMethods.map(async (method) => {
148 const request = http
149 .request({
150 method: method,
151 hostname: env.SIDECAR_HOSTNAME,
152 port: env.DEFAULT_HEADERS_EXIST_PORT,
153 })
154 .end();
155 return once(request, 'response');
156 })
157 );
158 },
159};
160 
161// Test is taken from test/parallel/test-http-client-defaults.js
162export const testHttpClientDefaults = {
163 async test() {
164 {
165 const req = new http.ClientRequest({});
166 strictEqual(req.path, '/');
167 strictEqual(req.method, 'GET');
168 }
169 
170 {
171 const req = new http.ClientRequest({ method: '' });
172 strictEqual(req.path, '/');
173 strictEqual(req.method, 'GET');
174 }
175 
176 {
177 const req = new http.ClientRequest({ path: '' });
178 strictEqual(req.path, '/');
179 strictEqual(req.method, 'GET');
180 }
181 },
182};
183 
184// Test is taken from test/parallel/test-http-client-encoding.js
185export const testHttpClientEncoding = {
186 async test(_ctrl, env) {
187 const { promise, resolve } = Promise.withResolvers();
188 http
189 .request(
190 {
191 hostname: env.SIDECAR_HOSTNAME,
192 port: env.PONG_SERVER_PORT,
193 encoding: 'utf8',
194 },
195 (res) => {
196 let data = '';
197 res.on('data', (chunk) => (data += chunk));
198 res.on('end', () => {
199 strictEqual(data, 'pong');
200 resolve();
201 });
202 }
203 )
204 .end();
205 await promise;
206 },
207};
208 
209// Test is taken from test/parallel/test-http-client-headers-host-array.js
210export const testHttpClientHeadersHostArray = {
211 async test() {
212 const options = {
213 port: '80',
214 path: '/',
215 headers: {
216 host: [],
217 },
218 };
219 
220 throws(
221 () => {
222 http.request(options);
223 },
224 {
225 code: /ERR_INVALID_ARG_TYPE/,
226 },
227 'http request should throw when passing array as header host'
228 );
229 },
230};
231 
232// Test is taken from test/parallel/test-http-client-input-function.js
233export const testHttpClientInputFunction = {
234 async test(_ctrl, env) {
235 const { promise, resolve } = Promise.withResolvers();
236 const req = new http.ClientRequest(
237 { hostname: env.SIDECAR_HOSTNAME, port: env.PONG_SERVER_PORT },
238 (response) => {
239 let body = '';
240 response.setEncoding('utf8');
241 response.on('data', (chunk) => {
242 body += chunk;
243 });
244 
245 response.on('end', () => {
246 strictEqual(body, 'pong');
247 resolve();
248 });
249 }
250 );
251 
252 req.end();
253 await promise;
254 },
255};
256 
257// Test is taken from test/parallel/test-http-client-invalid-path.js
258export const testHttpClientInvalidPath = {
259 async test() {
260 throws(
261 () => {
262 http
263 .request({
264 path: '/thisisinvalid\uffe2',
265 })
266 .end();
267 },
268 {
269 code: 'ERR_UNESCAPED_CHARACTERS',
270 name: 'TypeError',
271 }
272 );
273 },
274};
275 
276// Test is taken from test/parallel/test-http-client-unescaped-path.js
277export const testHttpClientUnescapedPath = {
278 async test() {
279 for (let i = 0; i <= 32; i += 1) {
280 const path = `bad${String.fromCharCode(i)}path`;
281 throws(
282 () =>
283 http.get({ path }, () => {
284 throw new Error('This should not happen');
285 }),
286 {
287 code: 'ERR_UNESCAPED_CHARACTERS',
288 name: 'TypeError',
289 message: 'Request path contains unescaped characters',
290 }
291 );
292 }
293 },
294};
295 
296// Test is taken from test/parallel/test-http-request-arguments.js
297export const testHttpRequestArguments = {
298 async test(_ctrl, env) {
299 const { promise, resolve } = Promise.withResolvers();
300 http.get(
301 'http://example.com/testpath',
302 { hostname: env.SIDECAR_HOSTNAME, port: env.REQUEST_ARGUMENTS_PORT },
303 (res) => {
304 res.resume();
305 resolve();
306 }
307 );
308 await promise;
309 },
310};
311 
312// Test is taken from test/parallel/test-http-request-dont-override-options.js
313export const testHttpRequestDontOverrideOptions = {
314 async test(_ctrl, env) {
315 const { promise, resolve } = Promise.withResolvers();
316 const agent = new http.Agent();
317 agent.defaultPort = env.PONG_SERVER_PORT;
318 
319 // Options marked as explicitly undefined for readability
320 // in this test, they should STAY undefined as options should not
321 // be mutable / modified
322 const options = {
323 host: undefined,
324 hostname: env.SIDECAR_HOSTNAME,
325 port: undefined,
326 defaultPort: undefined,
327 path: undefined,
328 method: undefined,
329 agent: agent,
330 };
331 
332 http
333 .request(options, function (res) {
334 res.resume();
335 strictEqual(options.host, undefined);
336 strictEqual(options.hostname, env.SIDECAR_HOSTNAME);
337 strictEqual(options.port, undefined);
338 strictEqual(options.defaultPort, undefined);
339 strictEqual(options.path, undefined);
340 strictEqual(options.method, undefined);
341 resolve();
342 })
343 .end();
344 await promise;
345 },
346};
347 
348// Test is taken from test/parallel/test-http-request-host-header.js
349export const testHttpRequestHostHeader = {
350 async test(_ctrl, env) {
351 // From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
352 // A server MUST respond with a 400 (Bad Request) status code to any
353 // HTTP/1.1 request message that lacks a Host header field
354 const { promise, resolve } = Promise.withResolvers();
355 http.get(
356 {
357 hostname: env.SIDECAR_HOSTNAME,
358 port: env.HEADER_VALIDATION_SERVER_PORT,
359 headers: [],
360 },
361 (res) => {
362 strictEqual(res.statusCode, 400);
363 strictEqual(res.headers.connection, 'close');
364 resolve();
365 }
366 );
367 await promise;
368 },
369};
370 
371// Test is taken from test/parallel/test-http-request-invalid-method-error.js
372export const testHttpRequestInvalidMethodError = {
373 async test() {
374 throws(() => http.request({ method: '\0' }), {
375 code: 'ERR_INVALID_HTTP_TOKEN',
376 name: 'TypeError',
377 message: 'Method must be a valid HTTP token ["\u0000"]',
378 });
379 },
380};
381 
382// Test is taken from test/parallel/test-http-request-join-authorization-headers.js
383export const testHttpRequestJoinAuthorizationHeaders = {
384 async test(_ctrl, env) {
385 const { promise, resolve } = Promise.withResolvers();
386 http.get(
387 {
388 hostname: env.SIDECAR_HOSTNAME,
389 port: env.HELLO_WORLD_SERVER_PORT,
390 method: 'POST',
391 headers: [
392 'authorization',
393 '1',
394 'authorization',
395 '2',
396 'cookie',
397 'foo',
398 'cookie',
399 'bar',
400 ],
401 joinDuplicateHeaders: true,
402 path: '/join-duplicate-headers',
403 },
404 (res) => {
405 strictEqual(res.statusCode, 200);
406 strictEqual(res.headers.authorization, '3, 4');
407 strictEqual(res.headers.cookie, 'foo; bar');
408 resolve();
409 }
410 );
411 await promise;
412 },
413};
414 
415export const testGetExport = {
416 async test() {
417 const { get: getFn } = await import('node:http');
418 deepStrictEqual(get, getFn);
419 },
420};
421 
422export const testPostRequestBodyEcho = {
423 async test(_ctrl, env) {
424 const { promise, resolve, reject } = Promise.withResolvers();
425 const testData =
426 'Hello, this is test data for POST request with body echoing!';
427 
428 const req = http.request(
429 {
430 hostname: env.SIDECAR_HOSTNAME,
431 port: env.HELLO_WORLD_SERVER_PORT,
432 path: '/echo',
433 method: 'POST',
434 headers: {
435 'Content-Type': 'text/plain',
436 'Content-Length': Buffer.byteLength(testData),
437 },
438 },
439 (res) => {
440 let responseBody = '';
441 res.on('data', (chunk) => {
442 responseBody += chunk.toString();
443 });
444 res.on('end', () => {
445 try {
446 strictEqual(res.statusCode, 200);
447 strictEqual(
448 responseBody,
449 testData,
450 `Response body should match the request body. Expected: ${JSON.stringify(testData)}, Got: ${JSON.stringify(responseBody)}`
451 );
452 resolve();
453 } catch (err) {
454 reject(err);
455 }
456 });
457 }
458 );
459 
460 req.on('error', (err) => {
461 reject(new Error(`Request failed: ${err.message}`));
462 });
463 
464 req.write(testData);
465 req.end();
466 
467 await promise;
468 },
469};
470 
471export const testHttpGetTestSearchParams = {
472 async test(_ctrl, env) {
473 const { promise, resolve } = Promise.withResolvers();
474 http.get(
475 {
476 hostname: env.SIDECAR_HOSTNAME,
477 port: env.HELLO_WORLD_SERVER_PORT,
478 method: 'POST',
479 path: '/search-path?hello=world',
480 },
481 (res) => {
482 strictEqual(res.statusCode, 200);
483 strictEqual(res.headers.url, '/search-path?hello=world');
484 resolve();
485 }
486 );
487 await promise;
488 },
489};
490 
491// Regression test for https://github.com/cloudflare/workerd/issues/5148
492export const testBodyDataDuplicationRegression = {
493 async test(_ctrl, env) {
494 const { promise, resolve, reject } = Promise.withResolvers();
495 
496 http
497 .request(
498 {
499 hostname: env.SIDECAR_HOSTNAME,
500 port: env.HELLO_WORLD_SERVER_PORT,
501 path: '/echo',
502 method: 'post',
503 headers: {
504 'content-type': 'application/json;charset=utf-8',
505 },
506 },
507 (response) => {
508 strictEqual(response.statusCode, 200);
509 let expected = '';
510 response.on('data', (chunk) => (expected += chunk));
511 response.on('end', () => {
512 strictEqual(
513 expected,
514 '{"email":"posting-wrangler@email.mail","from":"wrangler"}'
515 );
516 resolve(response);
517 });
518 }
519 )
520 .on('error', reject)
521 .end(
522 Buffer.from(
523 JSON.stringify({
524 email: 'posting-wrangler@email.mail',
525 from: 'wrangler',
526 })
527 )
528 );
529 
530 await promise;
531 },
532};
533 
534// Verify that the http module does not automatically decompress response bodies.
535// Node.js http passes raw bytes through, leaving Content-Encoding handling to the caller.
536export const testHttpClientGzipResponseNotAutoDecompressed = {
537 async test(_ctrl, env) {
538 const { promise, resolve, reject } = Promise.withResolvers();
539 http.get(
540 {
541 hostname: env.SIDECAR_HOSTNAME,
542 port: env.GZIP_SERVER_PORT,
543 },
544 (res) => {
545 strictEqual(res.headers['content-encoding'], 'gzip');
546 const chunks = [];
547 res.on('data', (chunk) => chunks.push(chunk));
548 res.on('end', () => {
549 try {
550 const raw = Buffer.concat(chunks);
551 // The body should still be compressed — manually gunzipping
552 // should recover the original text.
553 const decompressed = gunzipSync(raw);
554 strictEqual(decompressed.toString(), 'hello from gzip server');
555 resolve();
556 } catch (err) {
557 reject(err);
558 }
559 });
560 }
561 );
562 await promise;
563 },
564};
565 
566// Relevant Node.js tests
567// - [ ] test/parallel/test-http-client-abort-destroy.js
568// - [ ] test/parallel/test-http-client-abort-event.js
569// - [ ] test/parallel/test-http-client-abort-keep-alive-destroy-res.js
570// - [ ] test/parallel/test-http-client-abort-keep-alive-queued-tcp-socket.js
571// - [ ] test/parallel/test-http-client-abort-keep-alive-queued-unix-socket.js
572// - [ ] test/parallel/test-http-client-abort-no-agent.js
573// - [ ] test/parallel/test-http-client-abort-response-event.js
574// - [ ] test/parallel/test-http-client-abort-unix-socket.js
575// - [ ] test/parallel/test-http-client-abort.js
576// - [ ] test/parallel/test-http-client-abort2.js
577// - [ ] test/parallel/test-http-client-abort3.js
578// - [ ] test/parallel/test-http-client-aborted-event.js
579// - [ ] test/parallel/test-http-client-agent-abort-close-event.js
580// - [ ] test/parallel/test-http-client-agent-end-close-event.js
581// - [ ] test/parallel/test-http-client-agent.js
582// - [ ] test/parallel/test-http-client-check-http-token.js
583// - [x] test/parallel/test-http-client-close-event.js
584// - [ ] test/parallel/test-http-client-close-with-default-agent.js
585// - [x] test/parallel/test-http-client-default-headers-exist.js
586// - [x] test/parallel/test-http-client-defaults.js
587// - [x] test/parallel/test-http-client-encoding.js
588// - [ ] test/parallel/test-http-client-finished.js
589// - [ ] test/parallel/test-http-client-get-url.js
590// - [ ] test/parallel/test-http-client-headers-array.js
591// - [x] test/parallel/test-http-client-headers-host-array.js
592// - [ ] test/parallel/test-http-client-incomingmessage-destroy.js
593// - [x] test/parallel/test-http-client-input-function.js
594// - [x] test/parallel/test-http-client-invalid-path.js
595// - [ ] test/parallel/test-http-client-keep-alive-hint.js
596// - [ ] test/parallel/test-http-client-keep-alive-release-before-finish.js
597// - [ ] test/parallel/test-http-client-override-global-agent.js
598// - [ ] test/parallel/test-http-client-race-2.js
599// - [ ] test/parallel/test-http-client-race.js
600// - [ ] test/parallel/test-http-client-read-in-error.js
601// - [ ] test/parallel/test-http-client-readable.js
602// - [ ] test/parallel/test-http-client-reject-chunked-with-content-length.js
603// - [ ] test/parallel/test-http-client-reject-cr-no-lf.js
604// - [ ] test/parallel/test-http-client-reject-unexpected-agent.js
605// - [ ] test/parallel/test-http-client-req-error-dont-double-fire.js
606// - [x] test/parallel/test-http-client-request-options.js
607// - [x] test/parallel/test-http-client-res-destroyed.js
608// - [x] test/parallel/test-http-client-response-timeout.js
609// - [x] test/parallel/test-http-client-set-timeout.js
610// - [ ] test/parallel/test-http-client-spurious-aborted.js
611// - [ ] test/parallel/test-http-client-timeout-agent.js
612// - [ ] test/parallel/test-http-client-timeout-connect-listener.js
613// - [ ] test/parallel/test-http-client-timeout-event.js
614// - [ ] test/parallel/test-http-client-timeout-on-connect.js
615// - [ ] test/parallel/test-http-client-timeout-option-listeners.js
616// - [ ] test/parallel/test-http-client-timeout-option-with-agent.js
617// - [ ] test/parallel/test-http-client-timeout-option.js
618// - [ ] test/parallel/test-http-client-timeout-with-data.js
619// - [ ] test/parallel/test-http-client-timeout.js
620// - [x] test/parallel/test-http-client-unescaped-path.js
621// - [x] test/parallel/test-http-request-arguments.js
622// - [x] test/parallel/test-http-request-dont-override-options.js
623// - [ ] test/parallel/test-http-request-end-twice.js
624// - [ ] test/parallel/test-http-request-end.js
625// - [x] test/parallel/test-http-request-host-header.js
626// - [x] test/parallel/test-http-request-invalid-method-error.js
627// - [x] test/parallel/test-http-request-join-authorization-headers.js
628// - [ ] test/parallel/test-http-request-large-payload.js
629// - [ ] test/parallel/test-http-request-smuggling-content-length.js
630 
631// Tests doesn't make sense for workerd:
632// - test/parallel/test-http-client-error-rawbytes.js
633// - test/parallel/test-http-client-immediate-error.js
634// - test/parallel/test-http-client-insecure-http-parser-error.js
635// - test/parallel/test-http-client-parse-error.js
636// - test/parallel/test-http-client-pipe-end.js
637// - test/parallel/test-http-client-response-domain.js
638// - test/parallel/test-http-client-set-timeout-after-end.js
639// - test/parallel/test-http-client-upload-buf.js
640// - test/parallel/test-http-client-upload.js
641// - test/parallel/test-http-client-with-create-connection.js
642// - test/parallel/test-http-request-method-delete-payload.js
643// - test/parallel/test-http-request-methods.js