Skip to content
File

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

javascript1341 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 http from 'node:http';
5import { WorkerEntrypoint } from 'cloudflare:workers';
6import { strictEqual, ok, throws, notStrictEqual, rejects } from 'node:assert';
7import { httpServerHandler, handleAsNodeRequest } from 'cloudflare:node';
8import { mock } from 'node:test';
9import url from 'node:url';
10import qs from 'node:querystring';
11 
12export const checkPortsSetCorrectly = {
13 test(_ctrl, env) {
14 const keys = ['PONG_SERVER_PORT'];
15 for (const key of keys) {
16 strictEqual(typeof env[key], 'string');
17 ok(env[key].length > 0);
18 }
19 },
20};
21 
22export class GlobalService extends WorkerEntrypoint {
23 async fetch(request) {
24 await rejects(handleAsNodeRequest({}, request), {
25 message: /Failed to determine port for server/,
26 });
27 await rejects(handleAsNodeRequest(1234, request), {
28 message: /^Http server with port 1234 not found/,
29 });
30 return await handleAsNodeRequest({ port: 9090 }, request);
31 }
32}
33 
34export const testHttpServerHandler = {
35 test() {
36 throws(() => httpServerHandler(null), {
37 message: /Server descriptor cannot be null or undefined/,
38 });
39 
40 throws(() => httpServerHandler({}), {
41 message: /Failed to determine port for server/,
42 });
43 },
44};
45 
46const globalServer = http.createServer((req, res) => {
47 res.writeHead(200);
48 res.end('Hello, World!');
49});
50 
51globalServer.listen(9090);
52 
53export const testGlobalHttpServe = {
54 async test(_ctrl, env) {
55 strictEqual(globalServer.listening, true);
56 strictEqual(globalServer.address().port, 9090);
57 
58 const res = await env.GLOBAL_SERVICE.fetch('https://cloudflare.com');
59 strictEqual(res.status, 200);
60 strictEqual(await res.text(), 'Hello, World!');
61 
62 globalServer.close();
63 },
64};
65 
66// Test is taken from test/parallel/test-http-server-async-dispose.js
67export const testHttpServerAsyncDispose = {
68 async test() {
69 const server = http.createServer();
70 
71 server.listen(8080);
72 ok(server.listening);
73 const closeFn = mock.fn();
74 server.on('close', closeFn);
75 await server[Symbol.asyncDispose]();
76 ok(!server.listening);
77 strictEqual(closeFn.mock.callCount(), 1);
78 },
79};
80 
81// Test is taken from test/parallel/test-http-server-incomingmessage-destroy.js
82export const testHttpServerIncomingMessageDestroy = {
83 async test(_ctrl, env) {
84 const onErrorFn = mock.fn();
85 await using server = http.createServer((req, res) => {
86 const path = req.url;
87 
88 ok('cloudflare' in req);
89 ok('cf' in req.cloudflare);
90 
91 if (path === '/destroy-with-error') {
92 req.on('error', (err) => {
93 res.statusCode = 400;
94 res.end('Request destroyed: ' + err.message);
95 });
96 req.destroy(new Error('Destroy test'));
97 } else if (path === '/destroy-without-error') {
98 req.once('error', onErrorFn);
99 req.on('close', () => {
100 res.statusCode = 200;
101 res.end('Request destroyed without error');
102 });
103 req.destroy();
104 } else if (path === '/response-destroy-with-error') {
105 res.destroy(new Error('Response destroy test'));
106 }
107 });
108 
109 server.listen(8080);
110 
111 {
112 const res = await env.SERVICE.fetch(
113 'https://cloudflare.com/destroy-with-error'
114 );
115 strictEqual(res.status, 400);
116 strictEqual(await res.text(), 'Request destroyed: Destroy test');
117 }
118 
119 {
120 const res = await env.SERVICE.fetch(
121 'https://cloudflare.com/destroy-without-error'
122 );
123 strictEqual(res.status, 200);
124 strictEqual(await res.text(), 'Request destroyed without error');
125 strictEqual(onErrorFn.mock.callCount(), 0);
126 }
127 },
128};
129 
130// Test is taken from test/parallel/test-http-server-method.query.js
131export const testHttpServerMethodQuery = {
132 async test(_ctrl, env) {
133 await using server = http.createServer((req, res) => {
134 strictEqual(req.method, 'QUERY');
135 res.end('OK');
136 });
137 server.listen(8080);
138 
139 const res = await env.SERVICE.fetch('https://cloudflare.com', {
140 method: 'QUERY',
141 });
142 strictEqual(res.status, 200);
143 strictEqual(await res.text(), 'OK');
144 },
145};
146 
147// Tests is taken from test/parallel/test-http-server-multiheaders.js
148export const testHttpServerMultiHeaders = {
149 async test(_ctrl, env) {
150 const { promise, resolve } = Promise.withResolvers();
151 const server = http.createServer(function (req, res) {
152 strictEqual(req.headers.accept, 'abc, def, ghijklmnopqrst');
153 strictEqual(req.headers.host, 'foo');
154 strictEqual(req.headers['www-authenticate'], 'foo, bar, baz');
155 strictEqual(req.headers['proxy-authenticate'], 'foo, bar, baz');
156 strictEqual(req.headers['x-foo'], 'bingo');
157 strictEqual(req.headers['x-bar'], 'banjo, bango');
158 strictEqual(req.headers['sec-websocket-protocol'], 'chat, share');
159 strictEqual(
160 req.headers['sec-websocket-extensions'],
161 'foo; 1, bar; 2, baz'
162 );
163 strictEqual(req.headers.constructor, 'foo, bar, baz');
164 
165 res.writeHead(200, { 'Content-Type': 'text/plain' });
166 res.end('EOF');
167 
168 server.close();
169 });
170 
171 server.listen(8080, async function () {
172 const res = await env.SERVICE.fetch('https://cloudflare.com', {
173 headers: [
174 ['accept', 'abc'],
175 ['accept', 'def'],
176 ['Accept', 'ghijklmnopqrst'],
177 ['host', 'foo'],
178 ['Host', 'bar'],
179 ['hOst', 'baz'],
180 ['www-authenticate', 'foo'],
181 ['WWW-Authenticate', 'bar'],
182 ['WWW-AUTHENTICATE', 'baz'],
183 ['proxy-authenticate', 'foo'],
184 ['Proxy-Authenticate', 'bar'],
185 ['PROXY-AUTHENTICATE', 'baz'],
186 ['x-foo', 'bingo'],
187 ['x-bar', 'banjo'],
188 ['x-bar', 'bango'],
189 ['sec-websocket-protocol', 'chat'],
190 ['sec-websocket-protocol', 'share'],
191 ['sec-websocket-extensions', 'foo; 1'],
192 ['sec-websocket-extensions', 'bar; 2'],
193 ['sec-websocket-extensions', 'baz'],
194 ['constructor', 'foo'],
195 ['constructor', 'bar'],
196 ['constructor', 'baz'],
197 ],
198 });
199 
200 strictEqual(res.status, 200);
201 strictEqual(res.headers.get('content-type'), 'text/plain');
202 strictEqual(await res.text(), 'EOF');
203 
204 resolve();
205 });
206 
207 await promise;
208 },
209};
210 
211// Test is taken from test/parallel/test-http-server-multiheaders2.js
212export const testHttpServerMultiHeaders2 = {
213 async test(_ctrl, env) {
214 // One difference between Node.js and Cloudflare workers is that Cookie is allowed
215 // to have multiple values but in Workers it is not supported.
216 const multipleAllowed = [
217 'Accept',
218 'Accept-Charset',
219 'Accept-Encoding',
220 'Accept-Language',
221 'Connection',
222 'DAV', // GH-2750
223 'Pragma', // GH-715
224 'Link', // GH-1187
225 'WWW-Authenticate', // GH-1083
226 'Proxy-Authenticate', // GH-4052
227 'Sec-Websocket-Extensions', // GH-2764
228 'Sec-Websocket-Protocol', // GH-2764
229 'Via', // GH-6660
230 
231 // not a special case, just making sure it's parsed correctly
232 'X-Forwarded-For',
233 
234 // Make sure that unspecified headers is treated as multiple
235 'Some-Random-Header',
236 'X-Some-Random-Header',
237 ];
238 
239 const multipleForbidden = [
240 'Content-Type',
241 'User-Agent',
242 'Referer',
243 'Host',
244 'Authorization',
245 'Proxy-Authorization',
246 'If-Modified-Since',
247 'If-Unmodified-Since',
248 'From',
249 'Location',
250 'Max-Forwards',
251 ];
252 
253 await using server = http.createServer(function (req, res) {
254 for (const header of multipleForbidden) {
255 const value = req.headers[header.toLowerCase()];
256 strictEqual(
257 value,
258 'foo',
259 `multiple forbidden header parsed incorrectly: ${header} with value: "${value}"`
260 );
261 }
262 for (const header of multipleAllowed) {
263 const sep = header.toLowerCase() === 'cookie' ? '; ' : ', ';
264 strictEqual(
265 req.headers[header.toLowerCase()],
266 `foo${sep}bar`,
267 `multiple allowed header parsed incorrectly: ${header}`
268 );
269 }
270 
271 res.writeHead(200, { 'Content-Type': 'text/plain' });
272 res.end('EOF');
273 });
274 
275 function makeHeader(value) {
276 return function (header) {
277 return [header, value];
278 };
279 }
280 
281 const headers = []
282 .concat(multipleAllowed.map(makeHeader('foo')))
283 .concat(multipleForbidden.map(makeHeader('foo')))
284 .concat(multipleAllowed.map(makeHeader('bar')))
285 .concat(multipleForbidden.map(makeHeader('bar')));
286 
287 server.listen(8080);
288 
289 const res = await env.SERVICE.fetch('https://cloudflare.com/', {
290 headers,
291 });
292 strictEqual(res.status, 200);
293 },
294};
295 
296// Test for RFC 7230 compliant header splitting with quoted strings and escaped characters
297export const testHttpServerQuotedStringHeaders = {
298 async test(_ctrl, env) {
299 await using server = http.createServer((req, res) => {
300 // Basic quoted strings with commas
301 strictEqual(req.headers['content-type'], 'text/plain; f="a, b, c"');
302 strictEqual(req.headers['authorization'], 'Bearer token="abc, def"');
303 strictEqual(
304 req.headers['proxy-authorization'],
305 'Basic realm="test, realm"'
306 );
307 
308 // Escaped characters in quoted strings
309 strictEqual(
310 req.headers['user-agent'],
311 'Mozilla/5.0; comment="has \\"quotes\\" and, commas"'
312 );
313 
314 res.writeHead(200, { 'content-type': 'text/plain' });
315 res.end('ok');
316 });
317 
318 server.listen(8080);
319 const res = await env.SERVICE.fetch('https://cloudflare.com', {
320 method: 'GET',
321 headers: [
322 // Basic quoted string tests
323 ['content-type', 'text/plain; f="a, b, c"'],
324 ['content-type', 'text/foo; a="1, 2, 3"'],
325 ['authorization', 'Bearer token="abc, def"'],
326 ['authorization', 'Bearer token="ghi, jkl"'],
327 ['proxy-authorization', 'Basic realm="test, realm"'],
328 ['proxy-authorization', 'Basic realm="another, realm"'],
329 // Escaped character tests
330 ['user-agent', 'Mozilla/5.0; comment="has \\"quotes\\" and, commas"'],
331 ['user-agent', 'Chrome/100.0; info="version \\"100\\""'],
332 ],
333 });
334 
335 strictEqual(res.status, 200);
336 strictEqual(await res.text(), 'ok');
337 },
338};
339 
340// Test is taken from test/parallel/test-http-server-non-utf8-header.js
341export const testHttpServerNonUtf8Header = {
342 async test(_ctrl, env) {
343 const nonUtf8Header = 'bรฅr';
344 const nonUtf8ToLatin1 = Buffer.from(nonUtf8Header).toString('latin1');
345 
346 {
347 await using server = http.createServer((req, res) => {
348 res.writeHead(200, [
349 'content-disposition',
350 Buffer.from(nonUtf8Header).toString('binary'),
351 ]);
352 res.end('hello');
353 });
354 
355 server.listen(8080);
356 const res = await env.SERVICE.fetch('https://cloudflare.com', {
357 method: 'GET',
358 });
359 strictEqual(res.status, 200);
360 strictEqual(res.headers.get('content-disposition'), nonUtf8ToLatin1);
361 }
362 
363 {
364 // Test multi-value header
365 await using server = http.createServer((req, res) => {
366 res.writeHead(200, [
367 'content-disposition',
368 [Buffer.from(nonUtf8Header).toString('binary')],
369 ]);
370 res.end('hello');
371 });
372 
373 server.listen(8080);
374 const res = await env.SERVICE.fetch('https://cloudflare.com');
375 strictEqual(res.status, 200);
376 strictEqual(res.headers.get('content-disposition'), nonUtf8ToLatin1);
377 }
378 
379 {
380 await using server = http.createServer((req, res) => {
381 res.writeHead(200, [
382 'Content-Length',
383 '5',
384 'content-disposition',
385 Buffer.from(nonUtf8Header).toString('binary'),
386 ]);
387 res.end('hello');
388 });
389 
390 server.listen(8080);
391 
392 const res = await env.SERVICE.fetch('https://cloudflare.com');
393 strictEqual(res.status, 200);
394 // The issue is that Content-Length causes different header encoding behavior
395 // We expect the raw bytes to be interpreted as UTF-8 by the fetch API
396 strictEqual(res.headers.get('content-disposition'), nonUtf8Header);
397 }
398 },
399};
400 
401// Test is taken from test/parallel/test-http-server-options-incoming-message.js
402export const testHttpServerOptionsIncomingMessage = {
403 async test(_ctrl, env) {
404 class MyIncomingMessage extends http.IncomingMessage {
405 getUserAgent() {
406 return this.headers['user-agent'] || 'unknown';
407 }
408 }
409 
410 await using server = http.createServer(
411 {
412 IncomingMessage: MyIncomingMessage,
413 },
414 (req, res) => {
415 strictEqual(req.getUserAgent(), 'node-test');
416 res.statusCode = 200;
417 res.end();
418 }
419 );
420 server.listen(8080);
421 
422 const res = await env.SERVICE.fetch('https://cloudflare.com', {
423 headers: { 'User-Agent': 'node-test' },
424 });
425 strictEqual(res.status, 200);
426 },
427};
428 
429// Test is taken from test/parallel/test-http-server-options-server-response.js
430export const testHttpServerOptionsServerResponse = {
431 async test(_ctrl, env) {
432 class MyServerResponse extends http.ServerResponse {
433 status(code) {
434 return this.writeHead(code, { 'Content-Type': 'text/plain' });
435 }
436 }
437 
438 await using server = http.createServer(
439 {
440 ServerResponse: MyServerResponse,
441 },
442 (_req, res) => {
443 res.status(200);
444 res.end();
445 }
446 );
447 server.listen(8080);
448 
449 const res = await env.SERVICE.fetch('https://cloudflare.com');
450 strictEqual(res.status, 200);
451 strictEqual(res.headers.get('content-type'), 'text/plain');
452 },
453};
454 
455// Test is taken from test/parallel/test-http-server-timeouts-validation.js
456export const testHttpServerTimeoutsValidation = {
457 async test() {
458 // This test validates that the HTTP server timeouts are properly validated and set.
459 
460 {
461 const server = http.createServer();
462 strictEqual(server.headersTimeout, 60000);
463 strictEqual(server.requestTimeout, 300000);
464 }
465 
466 {
467 const server = http.createServer({
468 headersTimeout: 10000,
469 requestTimeout: 20000,
470 });
471 strictEqual(server.headersTimeout, 10000);
472 strictEqual(server.requestTimeout, 20000);
473 }
474 
475 {
476 const server = http.createServer({
477 headersTimeout: 10000,
478 requestTimeout: 10000,
479 });
480 strictEqual(server.headersTimeout, 10000);
481 strictEqual(server.requestTimeout, 10000);
482 }
483 
484 {
485 const server = http.createServer({ headersTimeout: 10000 });
486 strictEqual(server.headersTimeout, 10000);
487 strictEqual(server.requestTimeout, 300000);
488 }
489 
490 {
491 const server = http.createServer({ requestTimeout: 20000 });
492 strictEqual(server.headersTimeout, 20000);
493 strictEqual(server.requestTimeout, 20000);
494 }
495 
496 {
497 const server = http.createServer({ requestTimeout: 100000 });
498 strictEqual(server.headersTimeout, 60000);
499 strictEqual(server.requestTimeout, 100000);
500 }
501 
502 {
503 throws(
504 () =>
505 http.createServer({ headersTimeout: 10000, requestTimeout: 1000 }),
506 { code: 'ERR_OUT_OF_RANGE' }
507 );
508 }
509 },
510};
511 
512// Test is taken from test/parallel/test-http-server-write-after-end.js
513export const testHttpServerWriteAfterEnd = {
514 async test(_ctrl, env) {
515 const { promise, resolve } = Promise.withResolvers();
516 await using server = http.createServer(handle);
517 
518 function handle(_req, res) {
519 res.write('hello');
520 res.end();
521 
522 queueMicrotask(() => {
523 res.write('world', (err) => {
524 strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END');
525 resolve();
526 });
527 });
528 }
529 
530 server.listen(8080);
531 await env.SERVICE.fetch('https://cloudflare.com');
532 await promise;
533 },
534};
535 
536// Test is taken from test/parallel/test-http-server-write-end-after-end.js
537export const testHttpServerWriteEndAfterEnd = {
538 async test(_ctrl, env) {
539 const { promise, resolve } = Promise.withResolvers();
540 const handle = mock.fn((req, res) => {
541 res.write('hello');
542 res.end();
543 
544 queueMicrotask(() => {
545 res.end('world');
546 res.write('world', (err) => {
547 strictEqual(err.code, 'ERR_STREAM_WRITE_AFTER_END');
548 resolve();
549 });
550 });
551 });
552 const server = http.createServer(handle);
553 server.listen(8080);
554 
555 await env.SERVICE.fetch('https://cloudflare.com');
556 await promise;
557 strictEqual(handle.mock.callCount(), 1);
558 server.close();
559 },
560};
561 
562export const testHandleZeroNullUndefinedPortNumber = {
563 async test() {
564 // Test zero port number.
565 {
566 const { promise, resolve } = Promise.withResolvers();
567 const server = http.createServer();
568 const listeningFn = mock.fn();
569 server.on('listening', listeningFn);
570 server.listen(0, () => {
571 ok(server.listening);
572 notStrictEqual(server.address().port, 0);
573 strictEqual(listeningFn.mock.callCount(), 1);
574 server.close();
575 resolve();
576 });
577 await promise;
578 }
579 // Test null/undefined port number.
580 {
581 const { promise, resolve } = Promise.withResolvers();
582 const server = http.createServer();
583 const listeningFn = mock.fn();
584 server.on('listening', listeningFn);
585 server.listen(() => {
586 ok(server.listening);
587 notStrictEqual(server.address().port, 0);
588 strictEqual(listeningFn.mock.callCount(), 1);
589 server.close();
590 resolve();
591 });
592 await promise;
593 }
594 },
595};
596 
597export const testInvalidPorts = {
598 async test() {
599 const server = http.createServer();
600 for (const value of [NaN, Infinity, -1, 1.1, 9999999]) {
601 throws(() => server.listen(value), {
602 code: 'ERR_SOCKET_BAD_PORT',
603 });
604 }
605 strictEqual(server.listening, false);
606 },
607};
608 
609export const consumeRequestPayloadData = {
610 async test(_ctrl, env) {
611 await using server = http.createServer((req, res) => {
612 const path = req.url;
613 
614 if (path === '/small') {
615 strictEqual(req.method, 'POST');
616 let data = '';
617 req.setEncoding('utf8');
618 req.on('data', (d) => (data += d));
619 req.on('end', () => {
620 strictEqual(data, 'hello world');
621 res.setHeaders(new Headers({ hello: 'world' }));
622 res.end(data + ' x2');
623 });
624 } else if (path === '/large-streaming') {
625 strictEqual(req.method, 'POST');
626 const dataEvents = [];
627 let totalBytes = 0;
628 
629 req.on('data', (chunk) => {
630 dataEvents.push({
631 size: chunk.length,
632 firstByte: chunk[0],
633 lastByte: chunk[chunk.length - 1],
634 });
635 totalBytes += chunk.length;
636 });
637 
638 req.on('end', () => {
639 res.writeHead(200, { 'Content-Type': 'application/json' });
640 res.end(
641 JSON.stringify({
642 totalEvents: dataEvents.length,
643 totalBytes,
644 firstEventSize: dataEvents[0]?.size,
645 lastEventSize: dataEvents[dataEvents.length - 1]?.size,
646 allSamePattern: dataEvents.every(
647 (e) => e.firstByte === e.lastByte
648 ),
649 })
650 );
651 });
652 }
653 });
654 
655 server.listen(8080);
656 
657 {
658 const res = await env.SERVICE.fetch('https://cloudflare.com/small', {
659 body: 'hello world',
660 method: 'POST',
661 });
662 strictEqual(res.status, 200);
663 strictEqual(await res.text(), 'hello world x2');
664 strictEqual(res.headers.get('hello'), 'world');
665 }
666 
667 {
668 const largeData = Buffer.alloc(256 * 1024, 123); // 256KB of byte value 123
669 const res = await env.SERVICE.fetch(
670 'https://cloudflare.com/large-streaming',
671 {
672 body: largeData,
673 method: 'POST',
674 }
675 );
676 strictEqual(res.status, 200);
677 
678 const result = JSON.parse(await res.text());
679 ok(
680 result.totalEvents > 1,
681 `Should have multiple data events, got ${result.totalEvents}`
682 );
683 strictEqual(result.totalBytes, 256 * 1024);
684 ok(
685 result.allSamePattern,
686 'All chunks should contain the same byte pattern'
687 );
688 ok(
689 result.firstEventSize > 0 && result.lastEventSize > 0,
690 'Events should have positive sizes'
691 );
692 }
693 },
694};
695 
696// Test large streaming responses and various data types
697export const testStreamingResponses = {
698 async test(_ctrl, env) {
699 await using server = http.createServer((req, res) => {
700 const path = req.url;
701 
702 if (path === '/large') {
703 // Test 1: Large payload streaming
704 const CHUNK_SIZE = 1024 * 64; // 64KB
705 const NUM_CHUNKS = 10; // 640KB total
706 res.writeHead(200, { 'X-Test': 'large' });
707 for (let i = 0; i < NUM_CHUNKS; i++) {
708 res.write(Buffer.alloc(CHUNK_SIZE, i % 256));
709 }
710 res.end();
711 } else if (path === '/echo') {
712 // Test 2: Echo with backpressure
713 res.writeHead(200);
714 req.pipe(res);
715 } else if (path === '/mixed') {
716 // Test 3: Mixed data types and implicit headers
717 res.setHeader('X-Custom', 'mixed');
718 res.write('string|');
719 res.write(Buffer.from('buffer|'));
720 res.write(new Uint8Array([85, 56, 124])); // "U8|"
721 res.write(''); // Empty
722 res.write('utf8-รฑ|', 'utf8');
723 setTimeout(() => res.end('async'), 10);
724 } else if (path === '/many') {
725 // Test 4: Many small writes
726 res.writeHead(200);
727 for (let i = 0; i < 100; i++) {
728 res.write(`${i}|`);
729 }
730 res.end('END');
731 }
732 });
733 
734 server.listen(8080);
735 
736 // Test 1: Large payload
737 const res1 = await env.SERVICE.fetch('https://cloudflare.com/large');
738 strictEqual(res1.status, 200);
739 const data1 = await res1.arrayBuffer();
740 strictEqual(data1.byteLength, 1024 * 64 * 10);
741 
742 // Test 2: Echo
743 const testData = Buffer.alloc(1024 * 128, 42);
744 const res2 = await env.SERVICE.fetch('https://cloudflare.com/echo', {
745 method: 'POST',
746 body: testData,
747 });
748 const echo = Buffer.from(await res2.arrayBuffer());
749 ok(echo.equals(testData));
750 
751 // Test 3: Mixed data types
752 const res3 = await env.SERVICE.fetch('https://cloudflare.com/mixed');
753 strictEqual(res3.status, 200);
754 strictEqual(res3.headers.get('X-Custom'), 'mixed');
755 strictEqual(await res3.text(), 'string|buffer|U8|utf8-รฑ|async');
756 
757 // Test 4: Many writes
758 const res4 = await env.SERVICE.fetch('https://cloudflare.com/many');
759 const text4 = await res4.text();
760 ok(text4.endsWith('98|99|END'));
761 },
762};
763 
764// Test Content-Length enforcement
765export const testContentLengthEnforcement = {
766 async test(_ctrl, env) {
767 await using server = http.createServer((req, res) => {
768 const path = req.url;
769 
770 if (path === '/too-few') {
771 res.writeHead(200, { 'Content-Length': '20' });
772 res.write('0123456789'); // Only 10 bytes
773 res.end();
774 } else if (path === '/too-many') {
775 res.writeHead(200, { 'Content-Length': '10' });
776 res.write('0123456789');
777 res.write('0123456789'); // 20 bytes total
778 res.end();
779 } else if (path === '/exact') {
780 res.writeHead(200, { 'Content-Length': '15' });
781 res.write('Hello ');
782 res.write('World!!!');
783 res.end('!');
784 }
785 });
786 
787 server.listen(8080);
788 
789 // Test 1: Too few bytes
790 {
791 const res = await env.SERVICE.fetch('https://cloudflare.com/too-few');
792 strictEqual(res.status, 200);
793 strictEqual(await res.text(), '0123456789');
794 }
795 
796 // Test 2: Too many bytes
797 {
798 const res = await env.SERVICE.fetch('https://cloudflare.com/too-many');
799 strictEqual(res.status, 200);
800 strictEqual(await res.text(), '0123456789');
801 }
802 
803 // Test 3: Exact match
804 {
805 const res = await env.SERVICE.fetch('https://cloudflare.com/exact');
806 strictEqual(res.status, 200);
807 strictEqual(await res.text(), 'Hello World!!!!');
808 }
809 },
810};
811 
812export const testCorkUncorkBasic = {
813 async test(_ctrl, env) {
814 await using server = http.createServer((req, res) => {
815 res.writeHead(200, { 'Content-Type': 'text/plain' });
816 
817 strictEqual(res.writableLength, 0);
818 res.cork();
819 strictEqual(res.writableLength, 0);
820 res.write('chunk1');
821 strictEqual(res.writableLength, 108);
822 res.write('chunk2');
823 strictEqual(res.writableLength, 114);
824 res.write('chunk3');
825 strictEqual(res.writableLength, 120);
826 res.uncork();
827 strictEqual(res.writableLength, 0);
828 
829 res.end('final');
830 });
831 
832 server.listen(8080);
833 
834 const res = await env.SERVICE.fetch('https://cloudflare.com');
835 strictEqual(res.status, 200);
836 strictEqual(await res.text(), 'chunk1chunk2chunk3final');
837 },
838};
839 
840export const testBackpressureSignaling = {
841 async test(_ctrl, env) {
842 const events = [];
843 
844 await using server = http.createServer((req, res) => {
845 res.writeHead(200, { 'Content-Type': 'application/octet-stream' });
846 
847 let writeCount = 0;
848 let drainCount = 0;
849 
850 res.on('drain', () => {
851 drainCount++;
852 events.push({ type: 'drain', count: drainCount });
853 continueWriting();
854 });
855 
856 const continueWriting = () => {
857 while (writeCount < 50) {
858 const chunk = Buffer.alloc(1024 * 32, writeCount % 256);
859 const canContinue = res.write(chunk);
860 
861 strictEqual(typeof canContinue, 'boolean');
862 events.push({
863 type: 'write',
864 canContinue,
865 writeCount,
866 writableLength: res.writableLength,
867 });
868 
869 writeCount++;
870 
871 if (!canContinue) {
872 events.push({ type: 'backpressure', writeCount });
873 return;
874 }
875 }
876 
877 res.end();
878 };
879 
880 continueWriting();
881 });
882 
883 server.listen(8080);
884 
885 const res = await env.SERVICE.fetch('https://cloudflare.com');
886 strictEqual(res.status, 200);
887 strictEqual((await res.arrayBuffer()).byteLength, 1638400);
888 
889 const writeEvents = events.filter((e) => e.type === 'write');
890 const backpressureEvents = events.filter((e) => e.type === 'backpressure');
891 const drainEvents = events.filter((e) => e.type === 'drain');
892 
893 strictEqual(writeEvents.length, 50);
894 
895 writeEvents.forEach((e) => {
896 strictEqual(typeof e.canContinue, 'boolean');
897 strictEqual(typeof e.writableLength, 'number');
898 ok(e.writableLength >= 0);
899 });
900 
901 if (backpressureEvents.length > 0) {
902 ok(
903 drainEvents.length > 0,
904 'Should emit drain events when backpressure occurs'
905 );
906 
907 for (let i = 0; i < backpressureEvents.length; i++) {
908 ok(
909 drainEvents[i],
910 `Should have corresponding drain event for backpressure ${i}`
911 );
912 }
913 }
914 },
915};
916 
917// Test is taken from test/parallel/test-http-server.js
918export const testHttpServer = {
919 async test(_ctrl, env) {
920 const invalid_options = ['foo', 42, true, []];
921 
922 for (const option of invalid_options) {
923 throws(
924 () => {
925 new http.Server(option);
926 },
927 {
928 code: 'ERR_INVALID_ARG_TYPE',
929 }
930 );
931 }
932 
933 let request_number = 0;
934 
935 await using server = http.createServer(function (req, res) {
936 res.id = request_number;
937 req.id = request_number++;
938 
939 strictEqual(res.req, req);
940 
941 if (req.id === 0) {
942 strictEqual(req.method, 'GET');
943 strictEqual(url.parse(req.url).pathname, '/hello');
944 strictEqual(qs.parse(url.parse(req.url).query).hello, 'world');
945 strictEqual(qs.parse(url.parse(req.url).query).foo, 'b==ar');
946 }
947 
948 if (req.id === 1) {
949 strictEqual(req.method, 'POST');
950 strictEqual(url.parse(req.url).pathname, '/quit');
951 }
952 
953 if (req.id === 2) {
954 strictEqual(req.headers['x-x'], 'foo');
955 }
956 
957 if (req.id === 3) {
958 strictEqual(req.headers['x-x'], 'bar');
959 this.close();
960 }
961 
962 setTimeout(function () {
963 res.writeHead(200, { 'Content-Type': 'text/plain' });
964 res.write(url.parse(req.url).pathname);
965 res.end();
966 }, 1);
967 });
968 server.listen(8080);
969 
970 server.httpAllowHalfOpen = true;
971 
972 const hello = await env.SERVICE.fetch(
973 'https://example.com/hello?hello=world&foo=b==ar'
974 );
975 strictEqual(hello.status, 200);
976 strictEqual(await hello.text(), '/hello');
977 
978 const quit = await env.SERVICE.fetch('https://example.com/quit', {
979 method: 'POST',
980 });
981 strictEqual(quit.status, 200);
982 strictEqual(await quit.text(), '/quit');
983 
984 const xxFoo = await env.SERVICE.fetch('https://example.com/', {
985 method: 'POST',
986 headers: {
987 'x-x': 'foo',
988 },
989 });
990 strictEqual(xxFoo.status, 200);
991 strictEqual(await xxFoo.text(), '/');
992 
993 const xxBar = await env.SERVICE.fetch('https://example.com/', {
994 method: 'POST',
995 headers: {
996 'x-x': 'bar',
997 },
998 });
999 strictEqual(xxBar.status, 200);
1000 strictEqual(await xxBar.text(), '/');
1001 
1002 strictEqual(request_number, 4);
1003 },
1004};
1005 
1006// Test multiple pipe destinations (Node.js feature that web streams don't support)
1007export const testMultiplePipeDestinations = {
1008 async test(_ctrl, env) {
1009 const { Writable } = await import('node:stream');
1010 
1011 await using server = http.createServer((req, res) => {
1012 const path = req.url;
1013 
1014 if (path === '/multipipe') {
1015 res.writeHead(200, { 'Content-Type': 'application/json' });
1016 
1017 // Create multiple writable destinations
1018 const dest1Data = [];
1019 const dest2Data = [];
1020 const dest3Data = [];
1021 
1022 const dest1 = new Writable({
1023 write(chunk, encoding, callback) {
1024 dest1Data.push(chunk);
1025 callback();
1026 },
1027 });
1028 
1029 const dest2 = new Writable({
1030 write(chunk, encoding, callback) {
1031 dest2Data.push(chunk);
1032 callback();
1033 },
1034 });
1035 
1036 const dest3 = new Writable({
1037 write(chunk, encoding, callback) {
1038 dest3Data.push(chunk);
1039 callback();
1040 },
1041 });
1042 
1043 // Set up finish handlers to track completion
1044 let finishedCount = 0;
1045 const onFinish = () => {
1046 finishedCount++;
1047 if (finishedCount === 3) {
1048 // All destinations finished, send response
1049 const result = {
1050 dest1: Buffer.concat(dest1Data).toString(),
1051 dest2: Buffer.concat(dest2Data).toString(),
1052 dest3: Buffer.concat(dest3Data).toString(),
1053 allSame:
1054 Buffer.concat(dest1Data).equals(Buffer.concat(dest2Data)) &&
1055 Buffer.concat(dest2Data).equals(Buffer.concat(dest3Data)),
1056 };
1057 res.end(JSON.stringify(result));
1058 }
1059 };
1060 
1061 dest1.on('finish', onFinish);
1062 dest2.on('finish', onFinish);
1063 dest3.on('finish', onFinish);
1064 
1065 // Pipe to multiple destinations - this is the key test!
1066 req.pipe(dest1);
1067 req.pipe(dest2);
1068 req.pipe(dest3);
1069 } else {
1070 res.writeHead(404);
1071 res.end('Not Found');
1072 }
1073 });
1074 
1075 server.listen(8080);
1076 
1077 // Send test data
1078 const testData =
1079 'Hello from multiple pipes! This data should reach all destinations.';
1080 const response = await env.SERVICE.fetch(
1081 'https://cloudflare.com/multipipe',
1082 {
1083 method: 'POST',
1084 body: testData,
1085 headers: {
1086 'Content-Type': 'text/plain',
1087 },
1088 }
1089 );
1090 
1091 strictEqual(response.status, 200);
1092 const result = await response.json();
1093 
1094 // Verify all destinations received the same data
1095 strictEqual(
1096 result.dest1,
1097 testData,
1098 'Destination 1 should receive correct data'
1099 );
1100 strictEqual(
1101 result.dest2,
1102 testData,
1103 'Destination 2 should receive correct data'
1104 );
1105 strictEqual(
1106 result.dest3,
1107 testData,
1108 'Destination 3 should receive correct data'
1109 );
1110 strictEqual(
1111 result.allSame,
1112 true,
1113 'All destinations should receive identical data'
1114 );
1115 },
1116};
1117 
1118export const testScheduled = {
1119 async test(_ctrl, env) {
1120 strictEqual(typeof env.SERVICE.scheduled, 'function');
1121 
1122 await env.SERVICE.scheduled({
1123 scheduledTime: Date.now(),
1124 cron: '0 0 * * *',
1125 });
1126 
1127 strictEqual(scheduledCallCount, 1);
1128 },
1129};
1130 
1131export const testConfigurableHighWaterMark = {
1132 async test(_ctrl, env) {
1133 {
1134 await using server = http.createServer({ highWaterMark: 9999 });
1135 strictEqual(server.highWaterMark, 9999);
1136 }
1137 
1138 {
1139 // Node.js supports 1.1 as a value for highWaterMark
1140 await using server = http.createServer({ highWaterMark: 1.11 });
1141 strictEqual(server.highWaterMark, 1.11);
1142 }
1143 
1144 {
1145 // Node.js omits negative values
1146 await using server = http.createServer({ highWaterMark: -1 });
1147 strictEqual(server.highWaterMark, 65536);
1148 }
1149 
1150 for (const highWaterMark of [null, 'hello world', ['merhaba dunya']]) {
1151 throws(() => http.createServer({ highWaterMark }), {
1152 code: 'ERR_INVALID_ARG_TYPE',
1153 });
1154 }
1155 },
1156};
1157 
1158export const testIncomingMessageSocket = {
1159 async test(_ctrl, env) {
1160 await using server = http.createServer((req, res) => {
1161 strictEqual(req.socket.encrypted, false);
1162 strictEqual(req.socket.localPort, 8080);
1163 strictEqual(req.socket.localAddress, '127.0.0.1');
1164 strictEqual(req.socket.remoteAddress, '127.0.0.1');
1165 strictEqual(typeof req.socket.remotePort, 'number');
1166 ok(req.socket.remotePort >= Math.pow(2, 15));
1167 ok(req.socket.remotePort <= Math.pow(2, 16));
1168 strictEqual(req.socket.remoteFamily, 'IPv4');
1169 ok('on' in req.socket);
1170 
1171 res.writeHead(200);
1172 res.end('Hello, World!');
1173 });
1174 
1175 server.listen(8080);
1176 
1177 const res = await env.SERVICE.fetch('https://cloudflare.com');
1178 strictEqual(res.status, 200);
1179 strictEqual(await res.text(), 'Hello, World!');
1180 },
1181};
1182 
1183// Test for header duplication bug where headers like "ttl: 60" become "ttl: TTL, 60"
1184export const testWebPushHeaderDuplication = {
1185 async test(_ctrl, env) {
1186 await using server = http.createServer((req, res) => {
1187 res.writeHead(200, { 'Content-Type': 'application/json' });
1188 res.end(
1189 JSON.stringify(
1190 Object.fromEntries(
1191 Object.entries(req.headers).filter(
1192 ([key, _]) =>
1193 key === 'ttl' || key === 'urgency' || key === 'topic'
1194 )
1195 )
1196 )
1197 );
1198 });
1199 
1200 server.listen(8080);
1201 
1202 const res = await env.SERVICE.fetch('https://google.com', {
1203 headers: {
1204 TTL: '60',
1205 Urgency: 'high',
1206 Topic: 'test',
1207 },
1208 });
1209 const body = await res.json();
1210 strictEqual(body.ttl, '60');
1211 strictEqual(body.urgency, 'high');
1212 strictEqual(body.topic, 'test');
1213 },
1214};
1215 
1216export const testHttpRequestFields = {
1217 async test(_ctrl, env) {
1218 await using server = http.createServer((req, res) => {
1219 strictEqual(req._consuming, false, 'Consuming should be false');
1220 strictEqual(req._dumped, false, 'Dumped should be false');
1221 strictEqual(req._paused, false, 'Paused should be false');
1222 res.end('OK');
1223 });
1224 server.listen(8080);
1225 await env.SERVICE.fetch('https://cloudflare.com');
1226 },
1227};
1228 
1229export const testBufferedDataEventEmission = {
1230 async test(_ctrl, env) {
1231 const dataFn = mock.fn((chunk) => {
1232 strictEqual(chunk instanceof Buffer, true, 'Chunk should be a Buffer');
1233 strictEqual(chunk.toString(), 'test data');
1234 });
1235 const errorFn = mock.fn();
1236 const endFn = mock.fn();
1237 
1238 await using server = http.createServer((req, res) => {
1239 // Test that data events are emitted when listener is attached after data is buffered
1240 dataFn.mock.resetCalls();
1241 endFn.mock.resetCalls();
1242 
1243 req.on('data', dataFn);
1244 req.on('error', errorFn);
1245 req.on('end', () => {
1246 endFn();
1247 res.end('OK');
1248 });
1249 });
1250 server.listen(8080);
1251 
1252 await env.SERVICE.fetch('https://cloudflare.com', {
1253 method: 'POST',
1254 body: 'test data',
1255 });
1256 strictEqual(
1257 dataFn.mock.callCount(),
1258 1,
1259 '.on("data") should have been called'
1260 );
1261 strictEqual(
1262 endFn.mock.callCount(),
1263 1,
1264 '.on("end") should have been called'
1265 );
1266 
1267 await env.SERVICE.fetch('https://cloudflare.com', {
1268 method: 'GET',
1269 });
1270 strictEqual(errorFn.mock.callCount(), 0, 'Error should not be emitted');
1271 strictEqual(dataFn.mock.callCount(), 0);
1272 strictEqual(endFn.mock.callCount(), 1);
1273 },
1274};
1275 
1276let scheduledCallCount = 0;
1277 
1278export default {
1279 fetch(request) {
1280 return handleAsNodeRequest(8080, request);
1281 },
1282 async scheduled(event) {
1283 scheduledCallCount++;
1284 strictEqual(typeof event.scheduledTime, 'number');
1285 strictEqual(typeof event.cron, 'string');
1286 },
1287};
1288 
1289// Relevant Node.js tests
1290// - [x] test/parallel/test-http-server-async-dispose.js
1291// - [ ] test/parallel/test-http-server-capture-rejections.js
1292// - [ ] test/parallel/test-http-server-close-idle-wait-response.js
1293// - [ ] test/parallel/test-http-server-close-idle.js
1294// - [ ] test/parallel/test-http-server-consumed-timeout.js
1295// - [ ] test/parallel/test-http-server-headers-timeout-delayed-headers.js
1296// - [ ] test/parallel/test-http-server-headers-timeout-interrupted-headers.js
1297// - [ ] test/parallel/test-http-server-headers-timeout-keepalive.js
1298// - [ ] test/parallel/test-http-server-headers-timeout-pipelining.js
1299// - [x] test/parallel/test-http-server-incomingmessage-destroy.js
1300// - [x] test/parallel/test-http-server-method.query.js
1301// - [x] test/parallel/test-http-server-multiheaders.js
1302// - [x] test/parallel/test-http-server-multiheaders2.js
1303// - [x] test/parallel/test-http-server-non-utf8-header.js
1304// - [x] test/parallel/test-http-server-options-incoming-message.js
1305// - [x] test/parallel/test-http-server-options-server-response.js
1306// - [ ] test/parallel/test-http-server-request-timeout-delayed-body.js
1307// - [ ] test/parallel/test-http-server-request-timeout-delayed-headers.js
1308// - [ ] test/parallel/test-http-server-request-timeout-interrupted-body.js
1309// - [ ] test/parallel/test-http-server-request-timeout-interrupted-headers.js
1310// - [ ] test/parallel/test-http-server-request-timeout-keepalive.js
1311// - [ ] test/parallel/test-http-server-request-timeout-pipelining.js
1312// - [ ] test/parallel/test-http-server-request-timeout-upgrade.js
1313// - [x] test/parallel/test-http-server-timeouts-validation.js
1314// - [x] test/parallel/test-http-server-write-after-end.js
1315// - [x] test/parallel/test-http-server-write-end-after-end.js
1316// - [x] test/parallel/test-http-server.js
1317 
1318// Tests that does not apply to workerd.
1319// - [ ] test/parallel/test-http-server-connection-list-when-close.js
1320// - [ ] test/parallel/test-http-server-connections-checking-leak.js
1321// - [ ] test/parallel/test-http-server-clear-timer.js
1322// - [ ] test/parallel/test-http-server-client-error.js
1323// - [ ] test/parallel/test-http-server-close-all.js
1324// - [ ] test/parallel/test-http-server-close-destroy-timeout.js
1325// - [ ] test/parallel/test-http-server-de-chunked-trailer.js
1326// - [ ] test/parallel/test-http-server-delete-parser.js
1327// - [ ] test/parallel/test-http-server-destroy-socket-on-client-error.js
1328// - [ ] test/parallel/test-http-server-keep-alive-defaults.js
1329// - [ ] test/parallel/test-http-server-keep-alive-max-requests-null.js
1330// - [ ] test/parallel/test-http-server-keep-alive-timeout.js
1331// - [ ] test/parallel/test-http-server-keepalive-end.js
1332// - [ ] test/parallel/test-http-server-keepalive-req-gc.js
1333// - [ ] test/parallel/test-http-server-options-highwatermark.js
1334// - [ ] test/parallel/test-http-server-multiple-client-error.js
1335// - [ ] test/parallel/test-http-server-reject-chunked-with-content-length.js
1336// - [ ] test/parallel/test-http-server-reject-cr-no-lf.js
1337// - [ ] test/parallel/test-http-server-response-standalone.js
1338// - [ ] test/parallel/test-http-server-stale-close.js
1339// - [ ] test/parallel/test-http-server-unconsume.js
1340// - [ ] test/parallel/test-http-server-unconsume-consume.js