Skip to content
File

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

javascript471 lines
1// Copyright (c) 2025 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
4import { throws, ok, strictEqual, deepStrictEqual } from 'node:assert';
5import { validateHeaderName, validateHeaderValue, METHODS } from 'node:http';
6import httpCommon from 'node:_http_common';
7import { inspect } from 'node:util';
8import http from 'node:http';
9 
10export const checkPortsSetCorrectly = {
11 test(ctrl, env, ctx) {
12 const keys = [
13 'SIDECAR_HOSTNAME',
14 'PONG_SERVER_PORT',
15 'ASD_SERVER_PORT',
16 'TIMEOUT_SERVER_PORT',
17 'HELLO_WORLD_SERVER_PORT',
18 'HEADER_VALIDATION_SERVER_PORT',
19 ];
20 for (const key of keys) {
21 strictEqual(typeof env[key], 'string');
22 ok(env[key].length > 0);
23 }
24 },
25};
26 
27// Tests are taken from
28// https://github.com/nodejs/node/blob/c514e8f781b2acedb6a2b42208d8f8f4d8392f09/test/parallel/test-http-header-validators.js
29export const testHttpHeaderValidators = {
30 async test() {
31 // Expected static methods
32 isFunc(validateHeaderName, 'validateHeaderName');
33 isFunc(validateHeaderValue, 'validateHeaderValue');
34 
35 // - when used with valid header names - should not throw
36 ['user-agent', 'USER-AGENT', 'User-Agent', 'x-forwarded-for'].forEach(
37 (name) => {
38 validateHeaderName(name);
39 }
40 );
41 
42 // - when used with invalid header names:
43 ['איקס-פורוורד-פור', 'x-forwarded-fםr'].forEach((name) => {
44 throws(() => validateHeaderName(name), {
45 code: 'ERR_INVALID_HTTP_TOKEN',
46 });
47 });
48 
49 // - when used with valid header values - should not throw
50 [
51 ['x-valid', 1],
52 ['x-valid', '1'],
53 ['x-valid', 'string'],
54 ].forEach(([name, value]) => {
55 validateHeaderValue(name, value);
56 });
57 
58 // - when used with invalid header values:
59 [
60 // [header, value, expectedCode]
61 ['x-undefined', undefined, 'ERR_HTTP_INVALID_HEADER_VALUE'],
62 ['x-bad-char', 'לא תקין', 'ERR_INVALID_CHAR'],
63 ].forEach(([name, value, code]) => {
64 throws(() => validateHeaderValue(name, value), { code });
65 });
66 
67 // Misc.
68 function isFunc(v, ttl) {
69 ok(v.constructor === Function, `${ttl} is expected to be a function`);
70 }
71 },
72};
73 
74// Tests are taken from
75// https://github.com/nodejs/node/blob/c514e8f781b2acedb6a2b42208d8f8f4d8392f09/test/parallel/test-http-invalidheaderfield2.js
76export const testInvalidHeaderField2 = {
77 async test() {
78 // Good header field names
79 [
80 'TCN',
81 'ETag',
82 'date',
83 'alt-svc',
84 'Content-Type',
85 '0',
86 'Set-Cookie2',
87 'Set_Cookie',
88 'foo`bar^',
89 'foo|bar',
90 '~foobar',
91 'FooBar!',
92 '#Foo',
93 '$et-Cookie',
94 '%%Test%%',
95 'Test&123',
96 "It's_fun",
97 '2*3',
98 '4+2',
99 '3.14159265359',
100 ].forEach(function (str) {
101 strictEqual(
102 httpCommon._checkIsHttpToken(str),
103 true,
104 `_checkIsHttpToken(${inspect(str)}) unexpectedly failed`
105 );
106 });
107 // Bad header field names
108 [
109 ':',
110 '@@',
111 '中文呢', // unicode
112 '((((())))',
113 ':alternate-protocol',
114 'alternate-protocol:',
115 'foo\nbar',
116 'foo\rbar',
117 'foo\r\nbar',
118 'foo\x00bar',
119 '\x7FMe!',
120 '{Start',
121 '(Start',
122 '[Start',
123 'End}',
124 'End)',
125 'End]',
126 '"Quote"',
127 'This,That',
128 ].forEach(function (str) {
129 strictEqual(
130 httpCommon._checkIsHttpToken(str),
131 false,
132 `_checkIsHttpToken(${inspect(str)}) unexpectedly succeeded`
133 );
134 });
135 
136 // Good header field values
137 [
138 'foo bar',
139 'foo\tbar',
140 '0123456789ABCdef',
141 '!@#$%^&*()-_=+\\;\':"[]{}<>,./?|~`',
142 ].forEach(function (str) {
143 strictEqual(
144 httpCommon._checkInvalidHeaderChar(str),
145 false,
146 `_checkInvalidHeaderChar(${inspect(str)}) unexpectedly failed`
147 );
148 });
149 
150 // Bad header field values
151 [
152 'foo\rbar',
153 'foo\nbar',
154 'foo\r\nbar',
155 '中文呢', // unicode
156 '\x7FMe!',
157 'Testing 123\x00',
158 'foo\vbar',
159 'Ding!\x07',
160 ].forEach(function (str) {
161 strictEqual(
162 httpCommon._checkInvalidHeaderChar(str),
163 true,
164 `_checkInvalidHeaderChar(${inspect(str)}) unexpectedly succeeded`
165 );
166 });
167 },
168};
169 
170// Tests are taken from
171// https://github.com/nodejs/node/blob/c514e8f781b2acedb6a2b42208d8f8f4d8392f09/test/parallel/test-http-methods.js
172export const testHttpMethods = {
173 async test() {
174 const methods = [
175 'ACL',
176 'BIND',
177 'CHECKOUT',
178 'CONNECT',
179 'COPY',
180 'DELETE',
181 'GET',
182 'HEAD',
183 'LINK',
184 'LOCK',
185 'M-SEARCH',
186 'MERGE',
187 'MKACTIVITY',
188 'MKCALENDAR',
189 'MKCOL',
190 'MOVE',
191 'NOTIFY',
192 'OPTIONS',
193 'PATCH',
194 'POST',
195 'PROPFIND',
196 'PROPPATCH',
197 'PURGE',
198 'PUT',
199 'QUERY',
200 'REBIND',
201 'REPORT',
202 'SEARCH',
203 'SOURCE',
204 'SUBSCRIBE',
205 'TRACE',
206 'UNBIND',
207 'UNLINK',
208 'UNLOCK',
209 'UNSUBSCRIBE',
210 ];
211 
212 deepStrictEqual(METHODS, methods.toSorted());
213 deepStrictEqual(httpCommon.methods, methods.toSorted());
214 },
215};
216 
217// Tests are taken from
218// https://github.com/nodejs/node/blob/c514e8f781b2acedb6a2b42208d8f8f4d8392f09/test/parallel/test-http-common.js
219export const testHttpCommon = {
220 async test() {
221 const checkIsHttpToken = httpCommon._checkIsHttpToken;
222 const checkInvalidHeaderChar = httpCommon._checkInvalidHeaderChar;
223 
224 // checkIsHttpToken
225 ok(checkIsHttpToken('t'));
226 ok(checkIsHttpToken('tt'));
227 ok(checkIsHttpToken('ttt'));
228 ok(checkIsHttpToken('tttt'));
229 ok(checkIsHttpToken('ttttt'));
230 ok(checkIsHttpToken('content-type'));
231 ok(checkIsHttpToken('etag'));
232 
233 strictEqual(checkIsHttpToken(''), false);
234 strictEqual(checkIsHttpToken(' '), false);
235 strictEqual(checkIsHttpToken('あ'), false);
236 strictEqual(checkIsHttpToken('あa'), false);
237 strictEqual(checkIsHttpToken('aaaaあaaaa'), false);
238 
239 // checkInvalidHeaderChar
240 ok(checkInvalidHeaderChar('あ'));
241 ok(checkInvalidHeaderChar('aaaaあaaaa'));
242 
243 strictEqual(checkInvalidHeaderChar(''), false);
244 strictEqual(checkInvalidHeaderChar(1), false);
245 strictEqual(checkInvalidHeaderChar(' '), false);
246 strictEqual(checkInvalidHeaderChar(false), false);
247 strictEqual(checkInvalidHeaderChar('t'), false);
248 strictEqual(checkInvalidHeaderChar('tt'), false);
249 strictEqual(checkInvalidHeaderChar('ttt'), false);
250 strictEqual(checkInvalidHeaderChar('tttt'), false);
251 strictEqual(checkInvalidHeaderChar('ttttt'), false);
252 },
253};
254 
255// Test is taken from test/parallel/test-http-request-invalid-method-error.js
256export const testHttpRequestInvalidMethodError = {
257 async test() {
258 throws(() => http.request({ method: '\0' }), {
259 code: 'ERR_INVALID_HTTP_TOKEN',
260 name: 'TypeError',
261 message: 'Method must be a valid HTTP token ["\u0000"]',
262 });
263 },
264};
265 
266// Test is taken from test/parallel/test-http-content-length.js
267export const testHttpContentLength = {
268 async test(_ctrl, env) {
269 const expectedHeadersEndWithData = {
270 connection: 'keep-alive',
271 'content-length': String('hello world'.length),
272 };
273 
274 const expectedHeadersEndNoData = {
275 connection: 'keep-alive',
276 'content-length': '0',
277 };
278 
279 const { promise, resolve } = Promise.withResolvers();
280 let req;
281 
282 req = http.request({
283 hostname: env.SIDECAR_HOSTNAME,
284 port: env.HELLO_WORLD_SERVER_PORT,
285 method: 'POST',
286 path: '/end-with-data',
287 });
288 req.removeHeader('Date');
289 req.end('hello world');
290 req.on('response', function (res) {
291 deepStrictEqual(res.headers, {
292 ...expectedHeadersEndWithData,
293 'keep-alive': 'timeout=1',
294 });
295 res.resume();
296 });
297 
298 req = http.request({
299 hostname: env.SIDECAR_HOSTNAME,
300 port: env.HELLO_WORLD_SERVER_PORT,
301 method: 'POST',
302 path: '/empty',
303 });
304 req.removeHeader('Date');
305 req.end();
306 req.on('response', function (res) {
307 deepStrictEqual(res.headers, {
308 ...expectedHeadersEndNoData,
309 'keep-alive': 'timeout=1',
310 });
311 res.resume();
312 resolve();
313 });
314 await promise;
315 },
316};
317 
318// Test is taken from test/parallel/test-http-contentLength0.js
319export const testHttpContentLength0 = {
320 async test(_ctrl, env) {
321 const { promise, resolve, reject } = Promise.withResolvers();
322 const request = http.request(
323 {
324 hostname: env.SIDECAR_HOSTNAME,
325 port: env.HELLO_WORLD_SERVER_PORT,
326 method: 'POST',
327 path: '/content-length0',
328 },
329 (response) => {
330 response.on('error', reject);
331 response.resume();
332 response.on('end', resolve);
333 }
334 );
335 request.on('error', reject);
336 request.end();
337 await promise;
338 },
339};
340 
341// Test is taken from test/parallel/test-http-dont-set-default-headers-with-set-header.js
342export const testHttpDontSetDefaultHeadersWithSetHeader = {
343 async test(_ctrl, env) {
344 const { promise, resolve, reject } = Promise.withResolvers();
345 const req = http.request({
346 method: 'POST',
347 hostname: env.SIDECAR_HOSTNAME,
348 port: env.HEADER_VALIDATION_SERVER_PORT,
349 setDefaultHeaders: false,
350 path: '/test-1',
351 });
352 
353 req.setHeader('test', 'value');
354 req.setHeader(
355 'HOST',
356 `${env.SIDECAR_HOSTNAME}:${env.HEADER_VALIDATION_SERVER_PORT}`
357 );
358 req.setHeader('foo', ['bar', 'baz']);
359 req.setHeader('connection', 'close');
360 req.on('response', resolve);
361 req.on('error', reject);
362 strictEqual(req.headersSent, false);
363 req.end();
364 await promise;
365 strictEqual(req.headersSent, true);
366 },
367};
368 
369// Test is taken from test/parallel/test-http-dont-set-default-headers-with-setHost.js
370export const testHttpDontSetDefaultHeadersWithSetHost = {
371 async test(_ctrl, env) {
372 const { promise, resolve, reject } = Promise.withResolvers();
373 http
374 .request({
375 method: 'POST',
376 hostname: env.SIDECAR_HOSTNAME,
377 port: env.HEADER_VALIDATION_SERVER_PORT,
378 setDefaultHeaders: false,
379 setHost: true,
380 path: '/test-2',
381 })
382 .on('error', reject)
383 .on('response', resolve)
384 .end();
385 await promise;
386 },
387};
388 
389// Test is taken from test/parallel/test-http-request-end-twice.js
390export const testHttpRequestEndTwice = {
391 async test(_ctrl, env) {
392 const { promise, resolve, reject } = Promise.withResolvers();
393 const req = http
394 .get(
395 {
396 hostname: env.SIDECAR_HOSTNAME,
397 port: env.HEADER_VALIDATION_SERVER_PORT,
398 },
399 function (res) {
400 res.on('error', reject).on('end', function () {
401 strictEqual(req.end(), req);
402 resolve();
403 });
404 res.resume();
405 }
406 )
407 .on('error', reject);
408 await promise;
409 },
410};
411 
412// Test is taken from test/parallel/test-http-set-timeout.js
413export const testHttpSetTimeout = {
414 async test(_ctrl, env) {
415 const { promise, resolve, reject } = Promise.withResolvers();
416 const request = http.get({
417 hostname: env.SIDECAR_HOSTNAME,
418 port: env.TIMEOUT_SERVER_PORT,
419 path: '/',
420 });
421 request.setTimeout(100);
422 request.on('error', reject);
423 request.on('timeout', resolve);
424 request.end();
425 await promise;
426 },
427};
428 
429export const httpRedirectsAreNotFollowed = {
430 async test() {
431 const { promise, resolve } = Promise.withResolvers();
432 const req = http.request(
433 {
434 port: 80,
435 method: 'GET',
436 protocol: 'http:',
437 hostname: 'cloudflare.com',
438 path: '/',
439 },
440 (res) => {
441 strictEqual(res.statusCode, 301);
442 resolve();
443 }
444 );
445 req.end();
446 await promise;
447 },
448};
449 
450export const testExports = {
451 async test() {
452 strictEqual(typeof http.WebSocket, 'function');
453 strictEqual(typeof http.CloseEvent, 'function');
454 strictEqual(typeof http.MessageEvent, 'function');
455 strictEqual(typeof http._connectionListener, 'function');
456 strictEqual(typeof http.setMaxIdleHTTPParsers, 'function');
457 },
458};
459 
460// The following tests does not make sense for workerd
461//
462// - [ ] test/parallel/test-http-parser-bad-ref.js
463// - [ ] test/parallel/test-http-parser-finish-error.js
464// - [ ] test/parallel/test-http-parser-free.js
465// - [ ] test/parallel/test-http-parser-freed-before-upgrade.js
466// - [ ] test/parallel/test-http-parser-lazy-loaded.js
467// - [ ] test/parallel/test-http-parser-memory-retention.js
468// - [ ] test/parallel/test-http-parser-multiple-execute.js
469// - [ ] test/parallel/test-http-parser-timeout-reset.js
470// - [ ] test/parallel/test-http-parser.js