Skip to content
File

Blob: src/node/internal/internal_http_server.ts

typescript815 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// Copyright Joyent and Node contributors. All rights reserved. MIT license.
5 
6// This module implements Node.js-compatible HTTP server functionality on top of
7// the fetch API due to workerd limitations. The key challenge is bridging Node.js's
8// stream-based API with the Fetch API's Request/Response model.
9//
10// The ServerResponse class implements a single-buffer strategy to minimize memory
11// usage when converting from Node.js streams to Fetch Response bodies:
12// - Pre-header data is temporarily buffered until headers are sent
13// - Post-header data streams directly without intermediate buffering
14// - Memory is freed immediately after the transition point
15 
16import {
17 ERR_METHOD_NOT_IMPLEMENTED,
18 ERR_HTTP_HEADERS_SENT,
19 ERR_HTTP_INVALID_STATUS_CODE,
20 ERR_INVALID_CHAR,
21 ERR_INVALID_ARG_VALUE,
22 ERR_OUT_OF_RANGE,
23 ERR_OPTION_NOT_IMPLEMENTED,
24 ERR_SERVER_ALREADY_LISTEN,
25} from 'node-internal:internal_errors';
26import { EventEmitter } from 'node-internal:events';
27import { getDefaultHighWaterMark } from 'node-internal:streams_state';
28import {
29 kUniqueHeaders,
30 OutgoingMessage,
31 parseUniqueHeadersOption,
32} from 'node-internal:internal_http_outgoing';
33import {
34 validateBoolean,
35 validateFunction,
36 validateInteger,
37 validateObject,
38 validatePort,
39 validateNumber,
40} from 'node-internal:validators';
41import { portMapper } from 'cloudflare-internal:http';
42import {
43 IncomingMessage,
44 setIncomingMessageSocket,
45 setIncomingRequestBody,
46} from 'node-internal:internal_http_incoming';
47import { STATUS_CODES } from 'node-internal:internal_http_constants';
48import {
49 kServerResponse,
50 kIncomingMessage,
51 splitHeaderValue,
52} from 'node-internal:internal_http_util';
53import {
54 kOutHeaders,
55 type WrittenDataBufferEntry,
56 type HeadersSentEvent,
57} from 'node-internal:internal_http_outgoing';
58import {
59 chunkExpression,
60 _checkInvalidHeaderChar,
61} from 'node-internal:internal_http';
62import { _normalizeArgs } from 'node-internal:internal_net';
63import { Buffer } from 'node-internal:internal_buffer';
64 
65import type {
66 Server as _Server,
67 ServerResponse as _ServerResponse,
68 RequestListener,
69 ServerOptions,
70 OutgoingHttpHeaders,
71 OutgoingHttpHeader,
72} from 'node:http';
73import type { Socket, AddressInfo } from 'node:net';
74 
75const enableNodejsHttpServerModules =
76 !!Cloudflare.compatibilityFlags['enable_nodejs_http_server_modules'];
77 
78export const kConnectionsCheckingInterval = Symbol(
79 'http.server.connectionsCheckingInterval'
80);
81 
82export type DataWrittenEvent = {
83 index: number;
84 entry: WrittenDataBufferEntry;
85};
86 
87// By default Node.js forbids the following headers to be joined by comma.
88// Cloudflare workers implementation of Server, uses Fetch and by default
89// fetch joins them. Therefore, we need to maintain this list of headers
90// to filter and only return the first match to be Node.js compatible.
91//
92// For more reference, here is a Node.js test that validates this behavior:
93// https://github.com/nodejs/node/blob/af77e4bf2f8bee0bc23f6ee129d6ca97511d34b9/test/parallel/test-http-server-multiheaders2.js
94const multipleForbiddenHeaders = [
95 'host',
96 'content-type',
97 'user-agent',
98 'referer',
99 'authorization',
100 'proxy-authorization',
101 'if-modified-since',
102 'if-unmodified-since',
103 'from',
104 'location',
105 'max-forwards',
106];
107 
108export class Server
109 extends EventEmitter
110 implements _Server, BaseWithHttpOptions
111{
112 // @ts-expect-error TS2416 Server is not assignable to same property in base type.
113 [kIncomingMessage]: typeof IncomingMessage = IncomingMessage;
114 // @ts-expect-error TS2416 Server is not assignable to same property in base type.
115 [kServerResponse]: typeof ServerResponse = ServerResponse;
116 [kConnectionsCheckingInterval]?: number;
117 [kUniqueHeaders]: Set<string> | null = null;
118 
119 // Similar option to this. Too lazy to write my own docs.
120 // http://www.squid-cache.org/Doc/config/half_closed_clients/
121 // https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
122 httpAllowHalfOpen = false;
123 timeout = 0;
124 maxHeadersCount: number | null = null;
125 maxRequestsPerSocket = 0;
126 connectionsCheckingInterval = 30_000;
127 requestTimeout: number = 300_000;
128 headersTimeout: number = 60_000;
129 requireHostHeader: boolean = false;
130 joinDuplicateHeaders: boolean = false;
131 rejectNonStandardBodyWrites: boolean = false;
132 keepAliveTimeout: number = 5_000;
133 keepAliveTimeoutBuffer: number = 1_000;
134 highWaterMark: number = getDefaultHighWaterMark();
135 #port: number | null = null;
136 
137 constructor(options?: ServerOptions, requestListener?: RequestListener) {
138 if (!enableNodejsHttpServerModules) {
139 throw new ERR_METHOD_NOT_IMPLEMENTED('Server');
140 }
141 super();
142 
143 if (options != null) {
144 // @ts-expect-error TS2345 TODO(soon): Find a better way to handle this type mismatch.
145 storeHTTPOptions.call(this, options);
146 }
147 
148 if (options?.highWaterMark !== undefined) {
149 validateNumber(options.highWaterMark, 'options.highWaterMark');
150 if (options.highWaterMark > 0) {
151 this.highWaterMark = options.highWaterMark;
152 }
153 }
154 
155 if (typeof options === 'function') {
156 requestListener = options;
157 options = {};
158 } else if (options == null) {
159 options = {};
160 } else {
161 validateObject(options, 'options');
162 }
163 
164 if (requestListener) {
165 this.on('request', requestListener);
166 }
167 
168 this[kUniqueHeaders] = parseUniqueHeadersOption(
169 options.uniqueHeaders as (string | string[])[]
170 );
171 }
172 
173 // Failing to call close() on a http server may result in the server being leaked.
174 // To prevent this, call close() when you're done with the server, or use
175 // explicit resource management. (example: await using s = createServer())
176 close(callback?: VoidFunction): this {
177 httpServerPreClose(this);
178 if (this.#port != null) {
179 portMapper.delete(this.#port);
180 this.#port = null;
181 }
182 if (typeof callback === 'function') {
183 this.once('close', callback);
184 }
185 queueMicrotask(() => {
186 this.emit('close');
187 });
188 return this;
189 }
190 
191 closeAllConnections(): void {
192 // It doesn't make sense to support this method.
193 // Leave it as a noop.
194 }
195 
196 closeIdleConnections(): void {
197 // It doesn't make sense to support this method.
198 // Leave it as a noop.
199 }
200 
201 setTimeout(
202 msecs?: number | ((socket: Socket) => void),
203 callback?: (socket: Socket) => void
204 ): this {
205 if (typeof msecs === 'function') {
206 callback = msecs;
207 msecs = undefined;
208 } else if (typeof msecs === 'number') {
209 this.timeout = msecs;
210 }
211 
212 if (typeof callback === 'function') {
213 this.once('timeout', callback);
214 }
215 return this;
216 }
217 
218 async #onRequest(
219 request: Request,
220 env: unknown,
221 ctx: unknown
222 ): Promise<Response> {
223 const { incoming, response } = this.#toReqRes(request, env, ctx);
224 try {
225 this.emit('connection', this, incoming);
226 this.emit('request', incoming, response);
227 return await getServerResponseFetchResponse(response);
228 } catch (error: unknown) {
229 response.destroy(error);
230 throw error;
231 }
232 }
233 
234 #toReqRes(
235 request: Request,
236 env: unknown,
237 ctx: unknown
238 ): {
239 incoming: IncomingMessage;
240 response: ServerResponse;
241 } {
242 const incoming = new this[kIncomingMessage]();
243 setIncomingMessageSocket(incoming, {
244 headers: request.headers,
245 localPort: this.#port as number,
246 });
247 const reqUrl = new URL(request.url);
248 incoming.url = reqUrl.pathname + reqUrl.search;
249 
250 const headers = [];
251 for (const [key, value] of request.headers) {
252 if (multipleForbiddenHeaders.includes(key)) {
253 // By default fetch implementation will join the following header values with a comma.
254 // But in order to be node.js compatible, we need to select the first if possible.
255 // Use RFC 7230 compliant splitting that respects quoted-string constructions.
256 headers.push(key, splitHeaderValue(value));
257 } else {
258 headers.push(key, value);
259 }
260 }
261 incoming._addHeaderLines(headers, headers.length);
262 
263 incoming.method = request.method;
264 setIncomingRequestBody(incoming, request.body);
265 
266 // We provide a way for users to access to the Cloudflare-specific
267 // request properties, such as `cf` for accessing Cloudflare-specific request metadata.
268 incoming.cloudflare = {
269 env,
270 ctx,
271 };
272 if ('cf' in request) {
273 incoming.cloudflare.cf = request.cf as Record<string, unknown>;
274 }
275 
276 const response = new this[kServerResponse](incoming, {
277 highWaterMark: this.highWaterMark,
278 });
279 return { incoming, response };
280 }
281 
282 // We only support the listen() variant where a port number is passed or left
283 // unspecified. Such cases are listen(), listen(0, () => {}), listen(() => {}) etc.
284 listen(...args: unknown[]): this {
285 const [options, callback] = _normalizeArgs(args);
286 let port: number | undefined;
287 if (typeof options.port === 'number' || typeof options.port === 'string') {
288 port = validatePort(options.port, 'options.port');
289 }
290 
291 // If port number is not provided, default to 0, just like Node.js
292 if (port == null) {
293 port = 0;
294 }
295 
296 if (this.#port != null || portMapper.has(port)) {
297 throw new ERR_SERVER_ALREADY_LISTEN();
298 }
299 
300 if (callback !== null) {
301 this.once('listening', callback as (...args: unknown[]) => unknown);
302 }
303 
304 this.#port = this.#findSuitablePort(port);
305 // @ts-expect-error TS2322 Type mismatch. Not needed.
306 portMapper.set(this.#port, { fetch: this.#onRequest.bind(this) });
307 queueMicrotask(() => {
308 // If any of the listening handlers (here and in any of the other queueMicrotask(...) instances here,
309 // if the listening handlers throw an error, that will end up being reported to
310 // reportError(...) and will cause the globalThis error event to be triggered.
311 this.emit('listening');
312 });
313 return this;
314 }
315 
316 #findSuitablePort(port: number): number {
317 // We don't have to check if portMapper has it because the caller
318 // already validates the uniqueness of the port and calls this method.
319 if (port !== 0) {
320 return port;
321 }
322 
323 // Let's try at most 10 times to find a suitable port.
324 // If we can't find by that time, let's bail and throw an error.
325 for (let i = 0; i < 10; i++) {
326 port = Math.floor(Math.random() * 65535) + 1;
327 if (!portMapper.has(port)) {
328 return port;
329 }
330 }
331 
332 // This is unlikely to happen, but just in case.
333 throw new Error('Failed to find a suitable port after 10 attempts');
334 }
335 
336 getConnections(callback?: (err: Error | null, count: number) => void): this {
337 if (callback) {
338 validateFunction(callback, 'callback');
339 queueMicrotask(() => {
340 callback(null, 0);
341 });
342 }
343 return this;
344 }
345 
346 ref(): this {
347 // It doesn't make sense to implement these at all as they are very specific to the way
348 // Node.js' event loop and process model works.
349 return this;
350 }
351 
352 unref(): this {
353 // It doesn't make sense to implement these at all as they are very specific to the way
354 // Node.js' event loop and process model works.
355 return this;
356 }
357 
358 get listening(): boolean {
359 return this.#port != null;
360 }
361 
362 // We always return 127.0.0.1 as the address().address.
363 address(): string | AddressInfo | null {
364 if (this.#port == null) return null;
365 return { port: this.#port, family: 'IPv4', address: '127.0.0.1' };
366 }
367 
368 get maxConnections(): number {
369 return Infinity;
370 }
371 
372 get connections(): number {
373 return 0;
374 }
375 
376 async [Symbol.asyncDispose](): Promise<void> {
377 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
378 const { promise, resolve } = Promise.withResolvers<void>();
379 this.close(resolve);
380 return promise;
381 }
382}
383 
384// We use this handler to not expose this.#fetchResponse to outside world.
385let getServerResponseFetchResponse: (
386 response: ServerResponse
387) => Promise<Response>;
388 
389// Data flow:
390// 1. Before headers are sent: Data is buffered in a chunks array
391// - MessageBuffer emits '_dataWritten' events with sequential indices (0, 1, 2...)
392// - Each chunk is stored at its index position
393// 2. When headers are sent: Create Response with ReadableStream
394// - Flush all buffered chunks to the stream
395// - Clear the array with chunks.length = 0 to free memory
396// - Set up listeners for future data
397// 3. After headers: Data streams directly without buffering
398// - New '_dataWritten' events are immediately enqueued to the stream
399// 4. Completion: 'finish' event closes the ReadableStream
400// @ts-expect-error TS2720 Trailers related methods/attributes are missing.
401export class ServerResponse<Req extends IncomingMessage = IncomingMessage>
402 extends OutgoingMessage
403 // @ts-expect-error TS2720 `rawTrailers` attribute is missing.
404 implements _ServerResponse<Req>
405{
406 override [kOutHeaders]: Record<string, [string, string | string[]]> | null =
407 null;
408 
409 statusCode = 200;
410 statusMessage = 'unknown';
411 
412 #fetchResponse: Promise<Response>;
413 
414 static {
415 getServerResponseFetchResponse = (
416 response: ServerResponse
417 ): Promise<Response> => {
418 return response.#fetchResponse;
419 };
420 }
421 
422 constructor(req: Req, options: ServerOptions = {}) {
423 if (!enableNodejsHttpServerModules) {
424 throw new ERR_METHOD_NOT_IMPLEMENTED('ServerResponse');
425 }
426 
427 super(req, options);
428 
429 if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
430 this.useChunkedEncodingByDefault = chunkExpression.test(
431 (req.headers.te as string | undefined) ?? ''
432 );
433 this.shouldKeepAlive = false;
434 }
435 
436 const { promise, resolve, reject } = Promise.withResolvers<Response>();
437 
438 let streamController: ReadableStreamController<Uint8Array> | null = null;
439 const chunks: (Buffer | Uint8Array)[] = [];
440 const state: { bytesWritten: number; contentLength: number | null } = {
441 bytesWritten: 0,
442 contentLength: null,
443 };
444 
445 const handleData = (events: DataWrittenEvent[]): void => {
446 for (const event of events) {
447 let chunk = this.#dataFromDataWrittenEvent(event);
448 
449 // Trim chunk if it would exceed content-length
450 if (
451 state.contentLength !== null &&
452 state.bytesWritten + chunk.length > state.contentLength
453 ) {
454 const remainingBytes = state.contentLength - state.bytesWritten;
455 if (remainingBytes > 0) {
456 chunk = chunk.slice(0, remainingBytes);
457 } else {
458 continue; // Skip this chunk entirely
459 }
460 }
461 
462 state.bytesWritten += chunk.length;
463 
464 if (streamController) {
465 if (chunk.length > 0) {
466 // @ts-expect-error TS2345 Buffer extends Uint8Array, but has ArrayBufferLike instead of ArrayBuffer.
467 streamController.enqueue(chunk);
468 }
469 } else {
470 chunks[event.index] = chunk;
471 }
472 }
473 };
474 
475 this.on('_dataWritten', handleData);
476 this.once('error', reject);
477 
478 this.once(
479 '_headersSent',
480 ({ statusCode, statusMessage, headers }: HeadersSentEvent) => {
481 for (const [name, value] of headers) {
482 // Optimization: Avoid unnecessary string comparison by checking length first
483 if (name.length === 14 && name.toLowerCase() === 'content-length') {
484 state.contentLength = parseInt(value, 10);
485 break;
486 }
487 }
488 
489 resolve(
490 this.#toFetchResponse({
491 statusCode,
492 statusText: statusMessage,
493 headers,
494 onStreamStart: (controller) => {
495 streamController = controller;
496 for (const chunk of chunks) {
497 // @ts-expect-error TS2345 Buffer extends Uint8Array, but has ArrayBufferLike instead of ArrayBuffer.
498 controller.enqueue(chunk);
499 }
500 chunks.length = 0;
501 },
502 })
503 );
504 
505 this._closed = true;
506 this.emit('close');
507 }
508 );
509 
510 this.#fetchResponse = promise;
511 }
512 
513 #toFetchResponse({
514 statusCode,
515 statusText,
516 headers,
517 onStreamStart,
518 }: {
519 statusCode: number;
520 statusText: string;
521 headers: Headers;
522 onStreamStart: (controller: ReadableStreamController<Uint8Array>) => void;
523 }): Response {
524 let body = null;
525 
526 if (this._hasBody) {
527 body = new ReadableStream<Uint8Array>({
528 type: 'bytes',
529 start: (controller): void => {
530 onStreamStart(controller);
531 this.once('finish', () => {
532 controller.close();
533 });
534 this.on('error', controller.error.bind(controller));
535 },
536 cancel: (reason: unknown): void => {
537 this.destroy(reason);
538 },
539 });
540 }
541 
542 return new Response(body, {
543 status: statusCode,
544 statusText,
545 headers,
546 });
547 }
548 
549 #dataFromDataWrittenEvent({
550 index,
551 entry: { data, encoding },
552 }: DataWrittenEvent): Buffer | Uint8Array {
553 if (typeof data === 'string') {
554 // First chunk includes headers - skip them
555 if (index === 0) {
556 data = data.slice(this.writtenHeaderBytes);
557 }
558 
559 return Buffer.from(data, encoding ?? undefined);
560 }
561 
562 return data ?? Buffer.alloc(0);
563 }
564 
565 assignSocket(_socket: Socket): void {
566 // We don't plan to support this method, since our
567 // implementation is based on fetch and not Node.js sockets.
568 throw new ERR_METHOD_NOT_IMPLEMENTED('assignSocket');
569 }
570 
571 detachSocket(_socket: Socket): void {
572 // We don't plan to support this method, since our
573 // implementation is based on fetch and not Node.js sockets.
574 throw new ERR_METHOD_NOT_IMPLEMENTED('detachSocket');
575 }
576 
577 writeContinue(_cb: VoidFunction): void {
578 // There is no path forward to support this with fetch or kj.
579 throw new ERR_METHOD_NOT_IMPLEMENTED('writeContinue');
580 }
581 
582 writeProcessing(_cb: VoidFunction): void {
583 // There is no path forward to support this with fetch or kj.
584 throw new ERR_METHOD_NOT_IMPLEMENTED('writeProcessing');
585 }
586 
587 writeEarlyHints(_hints: unknown, _cb: VoidFunction): void {
588 // There is no path forward to support this with fetch or kj.
589 throw new ERR_METHOD_NOT_IMPLEMENTED('writeEarlyHints');
590 }
591 
592 override _implicitHeader(): void {
593 this.writeHead(this.statusCode);
594 }
595 
596 writeHead(
597 statusCode: number,
598 reason?: string | OutgoingHttpHeaders | OutgoingHttpHeader[],
599 obj?: OutgoingHttpHeaders | OutgoingHttpHeader[]
600 ): this {
601 if (this._header) {
602 throw new ERR_HTTP_HEADERS_SENT('write');
603 }
604 
605 const originalStatusCode = statusCode;
606 
607 statusCode |= 0;
608 if (statusCode < 200 || statusCode > 999) {
609 // < 100 status codes are not supported by cloudflare workers.
610 throw new ERR_HTTP_INVALID_STATUS_CODE(originalStatusCode);
611 }
612 
613 if (typeof reason === 'string') {
614 // writeHead(statusCode, reasonPhrase[, headers])
615 this.statusMessage = reason;
616 } else {
617 // writeHead(statusCode[, headers])
618 this.statusMessage ||= STATUS_CODES[`${statusCode}`] || 'unknown';
619 obj ??= reason;
620 }
621 this.statusCode = statusCode;
622 
623 let headers;
624 if (this[kOutHeaders]) {
625 // Slow-case: when progressive API and header fields are passed.
626 if (Array.isArray(obj)) {
627 if (obj.length % 2 !== 0) {
628 throw new ERR_INVALID_ARG_VALUE('headers', obj);
629 }
630 
631 // Headers in obj should override previous headers but still
632 // allow explicit duplicates. To do so, we first remove any
633 // existing conflicts, then use appendHeader.
634 
635 for (let n = 0; n < obj.length; n += 2) {
636 this.removeHeader(`${obj[n]}`);
637 }
638 
639 for (let n = 0; n < obj.length; n += 2) {
640 this.appendHeader(`${obj[n]}`, obj[n + 1] as OutgoingHttpHeader);
641 }
642 } else if (obj) {
643 for (const key of Object.keys(obj)) {
644 if (obj[key]) {
645 this.setHeader(key, obj[key]);
646 }
647 }
648 }
649 // Only progressive api is used
650 headers = this[kOutHeaders];
651 } else {
652 // Only writeHead() called
653 headers = obj;
654 }
655 
656 if (_checkInvalidHeaderChar(this.statusMessage)) {
657 throw new ERR_INVALID_CHAR('statusMessage');
658 }
659 
660 // TODO(soon): Unnecessary additional complexity when we could just build
661 // the Headers object up directly and skip the additional string wrangling.
662 const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
663 
664 if (statusCode === 204 || statusCode === 304) {
665 // RFC 2616, 10.2.5:
666 // The 204 response MUST NOT include a message-body, and thus is always
667 // terminated by the first empty line after the header fields.
668 // RFC 2616, 10.3.5:
669 // The 304 response MUST NOT contain a message-body, and thus is always
670 // terminated by the first empty line after the header fields.
671 // RFC 2616, 10.1 Informational 1xx:
672 // This class of status code indicates a provisional response,
673 // consisting only of the Status-Line and optional headers, and is
674 // terminated by an empty line.
675 this._hasBody = false;
676 }
677 
678 // Convert headers to a compatible type for _storeHeader
679 const convertedHeaders =
680 headers && !Array.isArray(headers)
681 ? (headers as OutgoingHttpHeaders)
682 : headers;
683 this._storeHeader(statusLine, convertedHeaders ?? null);
684 
685 return this;
686 }
687 
688 writeHeader = this.writeHead.bind(this);
689}
690 
691export function setupConnectionsTracking(): void {
692 throw new ERR_METHOD_NOT_IMPLEMENTED('setupConnectionsTracking');
693}
694 
695export interface BaseWithHttpOptions<
696 IM extends IncomingMessage = IncomingMessage,
697 SR extends ServerResponse = ServerResponse,
698> {
699 [kIncomingMessage]: IM;
700 [kServerResponse]: SR;
701 requestTimeout: number;
702 headersTimeout: number;
703 requireHostHeader: boolean;
704 joinDuplicateHeaders: boolean;
705 rejectNonStandardBodyWrites: boolean;
706}
707 
708export function storeHTTPOptions(
709 this: BaseWithHttpOptions,
710 options: ServerOptions
711): void {
712 // @ts-expect-error TS2322 Type mismatch.
713 this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
714 // @ts-expect-error TS2322 Type mismatch.
715 this[kServerResponse] = options.ServerResponse || ServerResponse;
716 
717 const maxHeaderSize = options.maxHeaderSize;
718 if (maxHeaderSize !== undefined) {
719 validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
720 throw new ERR_OPTION_NOT_IMPLEMENTED('maxHeaderSize');
721 }
722 
723 const insecureHTTPParser = options.insecureHTTPParser;
724 if (insecureHTTPParser !== undefined) {
725 // If enabled it will use a HTTP parser with leniency flags enabled.
726 // Since our implementation does not use any http parser, and uses "fetch" API,
727 // it doesn't make sense to support this option.
728 validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
729 }
730 
731 const requestTimeout = options.requestTimeout;
732 if (requestTimeout !== undefined) {
733 validateInteger(requestTimeout, 'requestTimeout', 0);
734 this.requestTimeout = requestTimeout;
735 } else {
736 this.requestTimeout = 300_000; // 5 minutes
737 }
738 
739 const headersTimeout = options.headersTimeout;
740 if (headersTimeout !== undefined) {
741 validateInteger(headersTimeout, 'headersTimeout', 0);
742 this.headersTimeout = headersTimeout;
743 } else {
744 this.headersTimeout = Math.min(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout
745 }
746 
747 if (
748 this.requestTimeout > 0 &&
749 this.headersTimeout > 0 &&
750 this.headersTimeout > this.requestTimeout
751 ) {
752 throw new ERR_OUT_OF_RANGE(
753 'headersTimeout',
754 '<= requestTimeout',
755 headersTimeout
756 );
757 }
758 
759 const keepAliveTimeout = options.keepAliveTimeout;
760 if (keepAliveTimeout !== undefined) {
761 validateInteger(keepAliveTimeout, 'keepAliveTimeout', 0);
762 throw new ERR_OPTION_NOT_IMPLEMENTED('keepAliveTimeout');
763 }
764 
765 const keepAliveTimeoutBuffer = options.keepAliveTimeoutBuffer;
766 if (keepAliveTimeoutBuffer !== undefined) {
767 validateInteger(keepAliveTimeoutBuffer, 'keepAliveTimeoutBuffer');
768 throw new ERR_OPTION_NOT_IMPLEMENTED('keepAliveTimeoutBuffer');
769 }
770 
771 const connectionsCheckingInterval = options.connectionsCheckingInterval;
772 if (connectionsCheckingInterval !== undefined) {
773 validateInteger(
774 connectionsCheckingInterval,
775 'connectionsCheckingInterval',
776 0
777 );
778 throw new ERR_OPTION_NOT_IMPLEMENTED('connectionsCheckingInterval');
779 }
780 
781 const requireHostHeader = options.requireHostHeader;
782 if (requireHostHeader !== undefined) {
783 validateBoolean(requireHostHeader, 'options.requireHostHeader');
784 this.requireHostHeader = requireHostHeader;
785 } else {
786 this.requireHostHeader = true;
787 }
788 
789 const joinDuplicateHeaders = options.joinDuplicateHeaders;
790 if (joinDuplicateHeaders !== undefined) {
791 validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
792 }
793 this.joinDuplicateHeaders = joinDuplicateHeaders ?? false;
794 
795 const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites;
796 if (rejectNonStandardBodyWrites !== undefined) {
797 validateBoolean(
798 rejectNonStandardBodyWrites,
799 'options.rejectNonStandardBodyWrites'
800 );
801 this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites;
802 } else {
803 this.rejectNonStandardBodyWrites = false;
804 }
805}
806 
807export function _connectionListener(): void {
808 throw new ERR_METHOD_NOT_IMPLEMENTED('_connectionListener');
809}
810 
811export function httpServerPreClose(server: Server): void {
812 server.closeIdleConnections();
813 clearInterval(server[kConnectionsCheckingInterval]);
814}