File
Blob: src/node/internal/internal_http_agent.ts
| 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 | import { EventEmitter } from 'node-internal:events'; |
| 7 | import { validateOneOf, validateNumber } from 'node-internal:validators'; |
| 8 | import { ERR_METHOD_NOT_IMPLEMENTED } from 'node-internal:internal_errors'; |
| 9 | import type { |
| 10 | RequestOptions, |
| 11 | Agent as _Agent, |
| 12 | AgentOptions, |
| 13 | ClientRequest, |
| 14 | IncomingMessage, |
| 15 | } from 'node:http'; |
| 16 | import type { Socket, NetConnectOpts } from 'node:net'; |
| 17 | import type { Duplex } from 'node:stream'; |
| 18 | |
| 19 | // This is mostly a stub implementation. |
| 20 | // We don't intend to support the Agent API right now beyond providing a very limited stub API. |
| 21 | // |
| 22 | export class Agent extends EventEmitter implements _Agent { |
| 23 | defaultPort: number; |
| 24 | protocol: string; |
| 25 | |
| 26 | options: AgentOptions & { __proto__: null }; |
| 27 | agentKeepAliveTimeoutBuffer: number = 1000; |
| 28 | keepAliveMsecs: number = 1000; |
| 29 | keepAlive: boolean = false; |
| 30 | maxSockets: number; |
| 31 | maxFreeSockets: number; |
| 32 | scheduling: 'lifo' | 'fifo' = 'lifo'; |
| 33 | maxTotalSockets: number; |
| 34 | totalSocketCount: number; |
| 35 | readonly freeSockets: NodeJS.ReadOnlyDict<Socket[]> = {}; |
| 36 | readonly sockets: NodeJS.ReadOnlyDict<Socket[]> = {}; |
| 37 | readonly requests: NodeJS.ReadOnlyDict<IncomingMessage[]> = {}; |
| 38 | |
| 39 | constructor(options?: AgentOptions) { |
| 40 | super({}); |
| 41 | this.options = { __proto__: null, ...options }; |
| 42 | |
| 43 | // Match Node.js: read defaultPort and protocol from options with fallbacks, |
| 44 | // set path = null, and ensure proxyEnv key exists. |
| 45 | /* eslint-disable @typescript-eslint/no-unsafe-assignment, |
| 46 | @typescript-eslint/no-unsafe-member-access, |
| 47 | @typescript-eslint/no-explicit-any */ |
| 48 | const opts = this.options as any; |
| 49 | this.defaultPort = opts.defaultPort || 80; |
| 50 | this.protocol = opts.protocol || 'http:'; |
| 51 | opts.path = null; |
| 52 | if (!('proxyEnv' in opts)) { |
| 53 | opts.proxyEnv = undefined; |
| 54 | } |
| 55 | /* eslint-enable @typescript-eslint/no-unsafe-assignment, |
| 56 | @typescript-eslint/no-unsafe-member-access, |
| 57 | @typescript-eslint/no-explicit-any */ |
| 58 | |
| 59 | if (this.options.noDelay === undefined) this.options.noDelay = true; |
| 60 | |
| 61 | // Don't confuse net and make it think that we're connecting to a pipe |
| 62 | this.keepAliveMsecs = this.options.keepAliveMsecs || 1000; |
| 63 | this.keepAlive = this.options.keepAlive || false; |
| 64 | this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets; |
| 65 | this.maxFreeSockets = this.options.maxFreeSockets || 256; |
| 66 | this.scheduling = this.options.scheduling || 'lifo'; |
| 67 | this.totalSocketCount = 0; |
| 68 | |
| 69 | validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']); |
| 70 | |
| 71 | if (this.options.maxTotalSockets !== undefined) { |
| 72 | validateNumber(this.options.maxTotalSockets, 'maxTotalSockets', 1); |
| 73 | this.maxTotalSockets = this.options.maxTotalSockets; |
| 74 | } else { |
| 75 | this.maxTotalSockets = Infinity; |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | static defaultMaxSockets: number = Infinity; |
| 80 | |
| 81 | createConnection( |
| 82 | _options: NetConnectOpts, |
| 83 | _callback?: (err: Error | null, stream: Duplex) => void |
| 84 | ): Duplex { |
| 85 | // We can't create a connection like this because our implementation |
| 86 | // relies on fetch() not on node:net sockets. In node.js, method |
| 87 | // calls node:net's createConnection method. |
| 88 | throw new ERR_METHOD_NOT_IMPLEMENTED('createConnection'); |
| 89 | } |
| 90 | |
| 91 | getName(options: RequestOptions = {}): string { |
| 92 | let name = options.host || 'localhost'; |
| 93 | |
| 94 | name += ':'; |
| 95 | if (options.port) name += options.port.toString(); |
| 96 | |
| 97 | name += ':'; |
| 98 | if (options.localAddress) name += options.localAddress; |
| 99 | |
| 100 | // Pacify parallel/test-http-agent-getname by only appending |
| 101 | // the ':' when options.family is set. |
| 102 | if (options.family === 4 || options.family === 6) |
| 103 | name += `:${options.family}`; |
| 104 | |
| 105 | if (options.socketPath) name += `:${options.socketPath}`; |
| 106 | |
| 107 | return name; |
| 108 | } |
| 109 | |
| 110 | addRequest(_req: ClientRequest, _options: unknown): void { |
| 111 | // Not implemented. Acts as a no-op. |
| 112 | } |
| 113 | |
| 114 | createSocket( |
| 115 | _req: ClientRequest, |
| 116 | _options: unknown, |
| 117 | _callback: VoidFunction |
| 118 | ): void { |
| 119 | // Not implemented. Acts as a no-op. |
| 120 | } |
| 121 | |
| 122 | removeSocket(_socket: Socket): void { |
| 123 | // Not implemented. Acts as a no-op. |
| 124 | } |
| 125 | |
| 126 | keepSocketAlive(_socket: Socket): boolean { |
| 127 | // Not implemented. Acts as a no-op. |
| 128 | return false; |
| 129 | } |
| 130 | |
| 131 | reuseSocket(_socket: Socket, _req: ClientRequest): void { |
| 132 | // Not implemented. Acts as a no-op. |
| 133 | } |
| 134 | |
| 135 | destroy(_error?: Error): void { |
| 136 | // Not implemented. Acts as a no-op. |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | export const globalAgent = new Agent({ |
| 141 | keepAlive: true, |
| 142 | scheduling: 'lifo', |
| 143 | timeout: 5000, |
| 144 | }); |