UNPKG

got

Version:

Human-friendly and powerful HTTP request library for Node.js

1,249 lines (1,248 loc) 43.6 kB
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/naming-convention, @typescript-eslint/no-restricted-types, @typescript-eslint/no-deprecated, promise/prefer-await-to-then */ import { Buffer } from 'node:buffer'; import { EventEmitter } from 'node:events'; import http, { validateHeaderName, validateHeaderValue, } from 'node:http'; import http2 from 'node:http2'; import https from 'node:https'; import { Writable, Readable } from 'node:stream'; import tls from 'node:tls'; import { urlToHttpOptions } from 'node:url'; import net from 'node:net'; import { TimeoutError } from '../timed-out.js'; const { HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_SCHEME, HTTP2_HEADER_STATUS, HTTP2_METHOD_CONNECT, NGHTTP2_CANCEL, } = http2.constants; const maxProtocolCacheSize = 100; const protocolCache = new Map(); const referenceIds = new WeakMap(); let nextReferenceId = 0; const connectionSpecificHeaders = new Set([ 'connection', 'http2-settings', 'keep-alive', 'proxy-connection', 'transfer-encoding', 'upgrade', ]); const normalizeRequestHeaderName = (name) => name.toLowerCase() === 'host' ? HTTP2_HEADER_AUTHORITY : name.toLowerCase(); const getConnectionHeaderTokens = (value) => { if (value === undefined) { return []; } const values = Array.isArray(value) ? value : [value]; const tokens = []; for (const item of values) { for (const token of String(item).split(',')) { const normalizedToken = token.trim().toLowerCase(); if (normalizedToken.length > 0) { tokens.push(normalizedToken); } } } return tokens; }; const isTrailersTeHeader = (value) => { if (value === undefined) { return true; } if (Array.isArray(value)) { return value.length === 1 && value[0].toLowerCase() === 'trailers'; } return String(value).trim().toLowerCase() === 'trailers'; }; const setProtocolCache = (key, value) => { if (!protocolCache.has(key) && protocolCache.size >= maxProtocolCacheSize) { protocolCache.delete(protocolCache.keys().next().value); } protocolCache.set(key, value); }; const getReferenceId = (value) => { let referenceId = referenceIds.get(value); if (referenceId === undefined) { referenceId = nextReferenceId++; referenceIds.set(value, referenceId); } return referenceId; }; const isPlainObject = (value) => { const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; }; const serializeSessionOption = (value) => { if (value === undefined) { return ['undefined']; } if (value === null) { return ['null']; } if (typeof value === 'function') { return ['function', getReferenceId(value)]; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return [typeof value, value]; } if (typeof value === 'bigint') { return ['bigint', value.toString()]; } if (typeof value === 'symbol') { return ['symbol', value.description]; } if (Array.isArray(value)) { return ['array', value.map(item => serializeSessionOption(item))]; } if (typeof value === 'object') { if (ArrayBuffer.isView(value)) { return ['bytes', Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString('base64')]; } if (isPlainObject(value)) { return ['object', Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, serializeSessionOption(item)])]; } return ['object-reference', getReferenceId(value)]; } return [typeof value]; }; const serializeSessionOptions = (values) => JSON.stringify(values.map(value => serializeSessionOption(value))); const getTlsSessionOptions = (options) => [ options.ca, options.cert, options.key, options.pfx, options.rejectUnauthorized, options.servername, options.localAddress, options.lookup, options.family, options.minVersion, options.maxVersion, options.ciphers, options.honorCipherOrder, options.checkServerIdentity, options.passphrase, options.sigalgs, options.sessionTimeout, options.dhparam, options.ecdhCurve, options.crl, options.secureOptions, options.secureContext, options.secureProtocol, ]; const destroyReuseSocket = (options) => { options._reuseSocket?.destroy(); delete options._reuseSocket; delete options._reuseSocketShouldPool; }; const normalizeInput = (input, options, callback) => { let normalizedOptions; if (typeof input === 'string') { normalizedOptions = urlToHttpOptions(new URL(input)); } else if (input instanceof URL) { normalizedOptions = urlToHttpOptions(input); } else { normalizedOptions = { ...input }; } if (typeof options === 'function' || options === undefined) { callback = options; } else { normalizedOptions = { ...normalizedOptions, ...options, }; } return { options: normalizedOptions, callback }; }; const getAuthority = (options) => { const protocol = options.protocol ?? 'https:'; const defaultPort = protocol === 'https:' ? 443 : 80; if (options.hostname === undefined && options.host !== undefined) { try { const authority = new URL(`${protocol}//${options.host}`); const port = options.port ?? authority.port; authority.port = String(port === '' ? defaultPort : port); return authority; } catch { // Fall back to hostname normalization for values like bare IPv6 addresses. } } const hostname = options.hostname ?? options.host ?? 'localhost'; const port = options.port ?? defaultPort; const hostnameString = String(hostname); const normalizedHostname = hostnameString.startsWith('[') && hostnameString.endsWith(']') ? hostnameString.slice(1, -1) : hostnameString; const formattedHostname = net.isIP(normalizedHostname) === 6 ? `[${normalizedHostname}]` : normalizedHostname; const authority = new URL(`${protocol}//${formattedHostname}`); authority.port = String(port); return authority; }; const getAuthorityPort = (authority) => { if (authority.port !== '') { return Number(authority.port); } return authority.protocol === 'https:' ? 443 : 80; }; const getConnectionHostname = (authority) => { const { hostname } = authority; return hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname; }; const hasCustomHttpsAgent = (agent) => typeof agent === 'object' && 'https' in agent && agent.https !== undefined && agent.https !== false; const getProtocolCacheKey = (options) => { const authority = getAuthority(options); const protocols = (options.ALPNProtocols ?? ['h2', 'http/1.1']).join(','); return serializeSessionOptions([ authority.host, protocols, ...getTlsSessionOptions(options), ]); }; const resolveProtocol = async (options, sourceOptions) => { const cacheKey = getProtocolCacheKey(options); const cachedProtocol = options.createConnection ? undefined : protocolCache.get(cacheKey); if (cachedProtocol !== undefined) { return { alpnProtocol: cachedProtocol }; } const authority = getAuthority(options); const { path: _path, agent: _agent, h2session: _h2session, _reuseSocket, _reuseSocketShouldPool, _alpnSocket, _socketTimeout, timeout, createConnection, checkServerIdentity, ...connectionOptions } = options; const port = getAuthorityPort(authority); const hostname = getConnectionHostname(authority); const servername = options.servername ?? (net.isIP(hostname) === 0 ? hostname : undefined); const tlsOptions = { ...connectionOptions, ALPNProtocols: options.ALPNProtocols ?? ['h2', 'http/1.1'], host: hostname, port, servername, }; if (checkServerIdentity) { tlsOptions.checkServerIdentity = checkServerIdentity; } const socket = createConnection ? createConnection(tlsOptions, () => { }) : tls.connect(port, hostname, tlsOptions); options._alpnSocket = socket; sourceOptions._alpnSocket = socket; return new Promise((resolve, reject) => { let settled = false; let timeoutId; let removeAbortListener; let socketTimeoutApplied = false; const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); } removeAbortListener?.(); socket.off('secureConnect', onSecureConnect); socket.off('error', onError); socket.off('close', onClose); socket.off('connect', applySocketTimeout); socket.off('timeout', onSocketTimeout); if (socketTimeoutApplied) { socket.setTimeout(0); } delete options._alpnSocket; delete sourceOptions._alpnSocket; }; const rejectOnce = (error) => { if (settled) { return; } settled = true; cleanup(); socket.destroy(); reject(error); }; const onSecureConnect = () => { if (settled) { return; } settled = true; cleanup(); const alpnProtocol = socket.alpnProtocol ?? false; if (!options.createConnection) { setProtocolCache(cacheKey, alpnProtocol); } resolve({ alpnProtocol, socket }); }; const onError = (error) => { rejectOnce(error); }; const onTimeout = () => { rejectOnce(new TimeoutError(Number(timeout), 'request')); }; const onSocketTimeout = () => { rejectOnce(new TimeoutError(_socketTimeout, 'socket')); }; const applySocketTimeout = () => { socketTimeoutApplied = true; socket.setTimeout(_socketTimeout); socket.once('timeout', onSocketTimeout); }; const onClose = () => { const error = new Error('The HTTP/2 ALPN socket closed before negotiation completed'); error.code = 'ECONNRESET'; rejectOnce(error); }; const onAbort = () => { const error = new Error('This operation was aborted.'); error.name = 'AbortError'; error.code = 'ERR_ABORTED'; rejectOnce(error); }; if (options.signal?.aborted) { onAbort(); return; } socket.once('secureConnect', onSecureConnect); socket.once('error', onError); socket.once('close', onClose); if (_socketTimeout !== undefined) { if (socket.connecting) { socket.once('connect', applySocketTimeout); } else { applySocketTimeout(); } } if (options.signal) { options.signal.addEventListener('abort', onAbort, { once: true }); removeAbortListener = () => { options.signal?.removeEventListener('abort', onAbort); }; } if (timeout !== undefined) { timeoutId = setTimeout(onTimeout, Number(timeout)); timeoutId.unref(); } }); }; const toRawHeaders = (headers) => { const rawHeaders = []; for (const [key, value] of Object.entries(headers)) { if (key.startsWith(':') || value === undefined) { continue; } if (Array.isArray(value)) { for (const item of value) { rawHeaders.push(key, item); } } else { rawHeaders.push(key, String(value)); } } return rawHeaders; }; const filterRawHeaders = (rawHeaders) => { const filteredHeaders = []; for (let index = 0; index < rawHeaders.length; index += 2) { const key = rawHeaders[index]; if (key.startsWith(':')) { continue; } filteredHeaders.push(key, rawHeaders[index + 1]); } return filteredHeaders; }; const filterHeaders = (headers) => { const filteredHeaders = {}; for (const [key, value] of Object.entries(headers)) { if (key.startsWith(':') || value === undefined) { continue; } filteredHeaders[key] = value; } return filteredHeaders; }; const createSocketProxy = (stream) => new Proxy(stream.session.socket, { get(target, property, receiver) { if (property === 'destroy') { return stream.destroy.bind(stream); } if (property === 'destroyed') { return stream.destroyed; } if (property === 'setTimeout') { return stream.setTimeout.bind(stream); } return Reflect.get(target, property, receiver); }, }); class Http2IncomingMessage extends Readable { stream; req; constructor(stream, request_, highWaterMark) { super({ autoDestroy: true, emitClose: false, highWaterMark, }); this.stream = stream; this.req = request_; this.socket = request_.socket; } aborted = false; httpVersion = '2.0'; httpVersionMajor = 2; httpVersionMinor = 0; complete = false; rawHeaders = []; rawTrailers = []; headers = {}; trailers = {}; statusCode; statusMessage; socket; url = ''; method; upgrade = false; get connection() { return this.socket; } set connection(value) { this.socket = value; } setTimeout(ms, callback) { this.req.setTimeout(ms, callback); return this; } _destroy(error, callback) { if (!this.readableEnded) { this.aborted = true; } this.stream.destroy(error ?? undefined); callback(); } _read() { this.stream.resume(); } _dump() { this.removeAllListeners('data'); this.resume(); } } export class Http2Agent extends EventEmitter { timeout; maxSessions; maxEmptySessions; sessions = new Map(); pendingSessions = new Set(); pendingSessionKeys = new Set(); queue = []; emptySessionCount = 0; sessionCount = 0; settings = { enablePush: false, initialWindowSize: 1024 * 1024 * 32, }; constructor({ timeout = 0, maxSessions = Number.POSITIVE_INFINITY, maxEmptySessions = 10 } = {}) { super(); this.timeout = timeout; this.maxSessions = maxSessions; this.maxEmptySessions = maxEmptySessions; } get protocol() { return 'https:'; } async request(origin, options, headers, streamOptions) { const { session, reusedSocket } = await this.getSessionWithMetadata(origin, options, true); return { stream: session.request(headers, streamOptions), reusedSocket, }; } async getSession(origin, options = {}) { const { session } = await this.getSessionWithMetadata(origin, options); return session; } async getSessionWithMetadata(origin, options = {}, reserveStream = false) { const normalizedOrigin = typeof origin === 'string' ? new URL(origin) : origin; const key = this.normalizeOptions(normalizedOrigin, options); const canUsePooledSession = options._reuseSocket === undefined || options._reuseSocketShouldPool === true; const session = canUsePooledSession ? this.getAvailableSession(key) : undefined; if (session) { if (reserveStream) { this.reserveStream(session); } destroyReuseSocket(options); return { session, reusedSocket: true, }; } return new Promise((resolve, reject) => { const entry = { origin: normalizedOrigin, options, reserveStream, resolve, reject, }; options._cancelSessionSetup = () => { const index = this.queue.indexOf(entry); if (index !== -1) { this.queue.splice(index, 1); delete options._cancelSessionSetup; destroyReuseSocket(options); reject(new Error('HTTP/2 session setup canceled')); } }; this.queue.push(entry); this.processQueue(); }); } normalizeOptions(origin, options = {}) { return serializeSessionOptions([ origin.origin, ...getTlsSessionOptions(options), ]); } closeEmptySessions(maxCount = Number.POSITIVE_INFINITY) { let closedCount = 0; for (const sessions of this.sessions.values()) { for (const session of sessions) { if ((session.currentStreamCount ?? 0) === 0) { closedCount++; session.close(); if (closedCount >= maxCount) { return closedCount; } } } } return closedCount; } destroy(reason) { for (const sessions of this.sessions.values()) { for (const session of sessions) { session.destroy(reason); } } for (const session of this.pendingSessions) { session.destroy(reason); } this.sessions.clear(); this.pendingSessionKeys.clear(); while (this.queue.length > 0) { const entry = this.queue.shift(); delete entry.options._cancelSessionSetup; destroyReuseSocket(entry.options); entry.reject(reason ?? new Error('Agent has been destroyed')); } } getAvailableSession(key) { const sessions = this.sessions.get(key); if (!sessions) { return; } return sessions.find(session => !session.destroyed && !session.closed && !session.gracefullyClosing && (session.currentStreamCount ?? 0) < (session.remoteSettings.maxConcurrentStreams ?? 100)); } processQueue() { let index = 0; while (index < this.queue.length) { const entry = this.queue[index]; const key = this.normalizeOptions(entry.origin, entry.options); const canUsePooledSession = entry.options._reuseSocket === undefined || entry.options._reuseSocketShouldPool === true; const session = canUsePooledSession ? this.getAvailableSession(key) : undefined; if (session) { this.queue.splice(index, 1); delete entry.options._cancelSessionSetup; if (entry.reserveStream) { this.reserveStream(session); } destroyReuseSocket(entry.options); entry.resolve({ session, reusedSocket: true, }); continue; } if (canUsePooledSession && this.pendingSessionKeys.has(key)) { index++; continue; } if (this.sessionCount >= this.maxSessions) { this.closeEmptySessions(this.sessionCount - this.maxSessions + 1); if (this.sessionCount >= this.maxSessions) { index++; continue; } } this.queue.splice(index, 1); if (canUsePooledSession) { this.pendingSessionKeys.add(key); } this.createSession(entry, key); } } reserveStream(session) { session.ref(); if (session.currentStreamCount === 0 && session.emptySessionCounted) { this.emptySessionCount--; session.emptySessionCounted = false; } session.currentStreamCount = (session.currentStreamCount ?? 0) + 1; session.reservedStreamCount = (session.reservedStreamCount ?? 0) + 1; } releaseStream(session, shouldPoolSession) { session.currentStreamCount = session.currentStreamCount - 1; if (session.currentStreamCount === 0) { if (!shouldPoolSession) { session.close(); this.processQueue(); return; } this.emptySessionCount++; session.emptySessionCounted = true; session.unref(); if (this.emptySessionCount > this.maxEmptySessions || session.gracefullyClosing) { session.close(); return; } } this.processQueue(); } createSession(entry, key) { this.sessionCount++; const { path: _path, agent: _agent, h2session: _h2session, _reuseSocketShouldPool, _socketTimeout, timeout: setupTimeout, ...sessionOptions } = entry.options; const options = { ...sessionOptions, settings: entry.options.settings ?? this.settings, ALPNProtocols: ['h2'], }; const reuseSocket = options._reuseSocket; const shouldPoolSession = reuseSocket === undefined || _reuseSocketShouldPool === true; if (options._reuseSocket) { options.createConnection = () => reuseSocket; delete options._reuseSocket; } let session; try { session = http2.connect(entry.origin, options); } catch (error) { this.sessionCount--; this.pendingSessionKeys.delete(key); delete entry.options._cancelSessionSetup; reuseSocket?.destroy(); entry.reject(error); this.processQueue(); return; } this.pendingSessions.add(session); session.currentStreamCount = 0; session.reservedStreamCount = 0; session.emptySessionCounted = false; session.gracefullyClosing = false; let settled = false; let sessionSetupTimeout; let socketTimeoutApplied = false; const sessionSocket = session.socket; const removeSessionSocketListener = sessionSocket.removeListener.bind(sessionSocket); const clearSessionSetupTimeout = () => { if (sessionSetupTimeout) { clearTimeout(sessionSetupTimeout); sessionSetupTimeout = undefined; } }; const clearSocketTimeout = () => { removeSessionSocketListener('connect', applySocketTimeout); if (!socketTimeoutApplied) { return; } socketTimeoutApplied = false; session.off('timeout', onSocketTimeout); if (!session.closed && !session.destroyed) { session.setTimeout(this.timeout); } }; const clearSessionSetup = () => { clearSessionSetupTimeout(); clearSocketTimeout(); this.pendingSessions.delete(session); this.pendingSessionKeys.delete(key); delete entry.options._cancelSessionSetup; }; const rejectSessionSetup = (error) => { if (settled) { return; } settled = true; clearSessionSetup(); entry.reject(error); queueMicrotask(() => { session.destroy(error); }); }; const onSocketTimeout = () => { rejectSessionSetup(new TimeoutError(_socketTimeout, 'socket')); }; const applySocketTimeout = () => { socketTimeoutApplied = true; session.setTimeout(_socketTimeout, onSocketTimeout); }; if (this.timeout > 0) { session.setTimeout(this.timeout, () => { session.destroy(); }); } if (_socketTimeout !== undefined) { if (sessionSocket.connecting) { sessionSocket.once('connect', applySocketTimeout); } else { applySocketTimeout(); } } if (setupTimeout !== undefined) { sessionSetupTimeout = setTimeout(() => { rejectSessionSetup(new TimeoutError(Number(setupTimeout), 'request')); }, Number(setupTimeout)); sessionSetupTimeout.unref(); } const removeSession = () => { if (session.emptySessionCounted) { this.emptySessionCount--; session.emptySessionCounted = false; } const sessions = this.sessions.get(key); if (sessions) { const index = sessions.indexOf(session); if (index !== -1) { sessions.splice(index, 1); } if (sessions.length === 0) { this.sessions.delete(key); } } }; session.once('remoteSettings', () => { if (settled) { return; } settled = true; clearSessionSetup(); if (shouldPoolSession) { const sessions = this.sessions.get(key) ?? []; sessions.push(session); this.sessions.set(key, sessions); } this.emit('session', session); if (entry.reserveStream) { this.reserveStream(session); } entry.resolve({ session, reusedSocket: false, }); this.processQueue(); }); session.once('error', error => { clearSessionSetup(); if (!settled) { settled = true; entry.reject(error); } removeSession(); }); session.once('goaway', () => { session.gracefullyClosing = true; if (session.currentStreamCount === 0) { session.close(); } }); session.once('close', () => { clearSessionSetup(); this.sessionCount--; if (!settled) { settled = true; entry.reject(new Error('The HTTP/2 session closed before settings were received')); } removeSession(); this.processQueue(); }); entry.options._cancelSessionSetup = () => { if (settled) { return; } settled = true; clearSessionSetup(); entry.reject(new Error('HTTP/2 session setup canceled')); session.destroy(); }; const request = session.request.bind(session); session.request = (headers, streamOptions) => { const hasReservedStream = (session.reservedStreamCount ?? 0) > 0; if (hasReservedStream) { session.reservedStreamCount = session.reservedStreamCount - 1; } if (session.gracefullyClosing) { if (hasReservedStream) { this.releaseStream(session, shouldPoolSession); } throw new Error('The session is gracefully closing. No new streams are allowed.'); } if (!hasReservedStream) { this.reserveStream(session); } let stream; try { stream = request(headers, streamOptions); } catch (error) { this.releaseStream(session, shouldPoolSession); throw error; } stream.once('close', () => { this.releaseStream(session, shouldPoolSession); }); return stream; }; } } class Http2ClientRequest extends Writable { constructor(input, options, callback) { super({ autoDestroy: false, emitClose: false, }); const normalized = normalizeInput(input, options, callback); this.options = normalized.options; this.callback = normalized.callback; this.method = (this.options.method ?? 'GET').toUpperCase(); this.path = this.method === HTTP2_METHOD_CONNECT ? String(this.options.path ?? '') : String(this.options.path ?? '/'); this.protocol = String(this.options.protocol ?? 'https:'); this.headers = Object.create(null); if (this.protocol !== 'https:' && !this.options.h2session) { throw new Error(`Protocol "${this.protocol}" not supported. Expected "https:"`); } const headers = this.options.headers; if (headers) { for (const [key, value] of Object.entries(headers)) { this.setHeader(key, value); } } if (this.options.auth && !this.hasHeader('authorization')) { this.setHeader('authorization', `Basic ${Buffer.from(this.options.auth).toString('base64')}`); } if (this.callback) { this.once('response', this.callback); } const authority = getAuthority(this.options); this.origin = authority; if (!this.hasHeader(HTTP2_HEADER_AUTHORITY)) { this.headers[HTTP2_HEADER_AUTHORITY] = this.method === HTTP2_METHOD_CONNECT ? this.path : authority.host; } this.headers[HTTP2_HEADER_METHOD] = this.method; if (this.method !== HTTP2_METHOD_CONNECT) { this.headers[HTTP2_HEADER_SCHEME] = this.protocol.slice(0, -1); this.headers[HTTP2_HEADER_PATH] = this.path; } } agent; aborted = false; reusedSocket = false; res; socket; connection; method; path; protocol; host; headersSent = false; maxHeadersCount; options; callback; headers; origin; stream; pendingJobs = []; pendingAgentPromise; connectionHeaderNames = new Set(); trailers; get isGotHttp2Request() { return true; } _write(chunk, encoding, callback) { const write = () => { this.stream.write(chunk, encoding, callback); }; if (this.stream) { write(); } else { this.pendingJobs.push({ run: write, cancel: callback, }); } void this.flushHeaders(); } _final(callback) { const end = () => { if (this.trailers) { this.stream.once('wantTrailers', () => { this.stream.sendTrailers(this.trailers); }); } this.stream.end(callback); }; if (this.stream) { end(); } else { this.pendingJobs.push({ run: end, cancel: callback, }); } void this.flushHeaders(); } _destroy(error, callback) { if (this.res && typeof this.res._dump === 'function') { this.res._dump(); } if (this.stream) { this.stream.close(NGHTTP2_CANCEL); } else { this.options._cancelSessionSetup?.(); if (error === null) { queueMicrotask(() => { this.emit('close'); }); } } if (this.pendingAgentPromise) { void this.pendingAgentPromise.catch(() => { }); } if (!this.stream) { this.cancelPendingJobs(error ?? new Error('The HTTP/2 request was destroyed before a stream was created')); } callback(error); } abort() { if (this.res?.complete) { return; } if (!this.aborted) { queueMicrotask(() => { this.emit('abort'); }); } this.aborted = true; this.destroy(); } async flushHeaders() { if (this.headersSent || this.destroyed) { return; } this.headersSent = true; try { if (this.options.h2session) { this.reusedSocket = true; this.onStream(this.options.h2session.request(this.headers, { endStream: false, waitForTrailers: this.trailers !== undefined, })); return; } this.agent = this.options.agent === false ? new Http2Agent({ maxEmptySessions: 0 }) : this.options.agent ?? globalAgent; const streamPromise = this.agent.request(this.origin, this.options, this.headers, { endStream: false, waitForTrailers: this.trailers !== undefined, }); this.pendingAgentPromise = streamPromise; const { stream, reusedSocket } = await streamPromise; this.reusedSocket = reusedSocket; this.onStream(stream); this.pendingAgentPromise = undefined; } catch (error) { this.pendingAgentPromise = undefined; this.destroy(error); } } addTrailers(headers) { if (this.headersSent) { throw new Error('Cannot add trailers after the HTTP/2 stream has been created'); } const trailers = Object.create(null); const connectionHeaderNames = new Set(); for (const [name, value] of Object.entries(headers)) { validateHeaderName(name); validateHeaderValue(name, value); const lowercasedName = name.toLowerCase(); if (lowercasedName === 'connection' || lowercasedName === 'proxy-connection') { for (const token of getConnectionHeaderTokens(value)) { connectionHeaderNames.add(token); Reflect.deleteProperty(trailers, token); } continue; } if (connectionSpecificHeaders.has(lowercasedName)) { continue; } if (connectionHeaderNames.has(lowercasedName)) { continue; } if (lowercasedName === 'te' && !isTrailersTeHeader(value)) { continue; } trailers[lowercasedName] = value; } this.trailers = trailers; } setHeader(name, value) { if (this.headersSent) { throw new Error('Cannot set headers after they are sent to the client'); } validateHeaderName(name); validateHeaderValue(name, value); const lowercasedName = name.toLowerCase(); if (lowercasedName === 'connection' || lowercasedName === 'proxy-connection') { for (const token of getConnectionHeaderTokens(value)) { this.connectionHeaderNames.add(token); Reflect.deleteProperty(this.headers, normalizeRequestHeaderName(token)); } return this; } if (connectionSpecificHeaders.has(lowercasedName)) { return this; } if (this.connectionHeaderNames.has(lowercasedName)) { return this; } if (lowercasedName === 'te' && !isTrailersTeHeader(value)) { return this; } this.headers[normalizeRequestHeaderName(name)] = value; return this; } getHeader(name) { return this.headers[normalizeRequestHeaderName(name)]; } getHeaders() { return { ...this.headers }; } getHeaderNames() { return Object.keys(this.headers); } hasHeader(name) { return this.getHeader(name) !== undefined; } removeHeader(name) { if (this.headersSent) { throw new Error('Cannot remove headers after they are sent to the client'); } Reflect.deleteProperty(this.headers, normalizeRequestHeaderName(name)); } setNoDelay() { } setSocketKeepAlive() { } setTimeout(ms, callback) { const applyTimeout = () => { this.stream.setTimeout(ms, callback); }; if (this.stream) { applyTimeout(); } else { this.pendingJobs.push({ run: applyTimeout }); } return this; } cancelPendingJobs(error) { const jobs = this.pendingJobs; this.pendingJobs = []; for (const job of jobs) { job.cancel?.(error); } } onStream(stream) { this.stream = stream; this.socket = createSocketProxy(stream); this.connection = this.socket; if (this.destroyed) { stream.destroy(); return; } stream.once('error', error => { if (this.destroyed) { return; } this.destroy(error); }); stream.once('aborted', () => { if (this.res) { this.res.aborted = true; this.res.emit('aborted'); this.res.destroy(); } else { this.destroy(new Error('The server aborted the HTTP/2 stream')); } }); stream.once('response', (headers, _flags, rawHeaders) => { const response = new Http2IncomingMessage(stream, this, stream.readableHighWaterMark); const incomingResponse = response; response.statusCode = Number(headers[HTTP2_HEADER_STATUS]); response.headers = filterHeaders(headers); response.rawHeaders = rawHeaders ? filterRawHeaders(rawHeaders) : toRawHeaders(headers); response.url = `${this.origin.origin}${this.path}`; incomingResponse.req = this; this.res = incomingResponse; stream.on('data', chunk => { if (!response.push(chunk)) { stream.pause(); } }); stream.once('end', () => { if (!this.aborted) { response.complete = true; response.push(null); } }); if (!this.emit('response', incomingResponse)) { response._dump(); } }); stream.on('headers', (headers, _flags, rawHeaders) => { this.emit('information', { statusCode: Number(headers[HTTP2_HEADER_STATUS]), statusMessage: '', httpVersion: '2.0', httpVersionMajor: 2, httpVersionMinor: 0, headers: filterHeaders(headers), rawHeaders: rawHeaders ? filterRawHeaders(rawHeaders) : toRawHeaders(headers), }); }); stream.once('trailers', (trailers, _flags, rawTrailers) => { if (!this.res) { return; } this.res.trailers = trailers; this.res.rawTrailers = Array.isArray(rawTrailers) ? rawTrailers : toRawHeaders(trailers); }); stream.once('close', () => { if (this.res) { if (this.aborted) { this.res.aborted = true; this.res.emit('aborted'); this.res.destroy(); } const finish = () => { this.res.emit('close'); this.destroy(); this.emit('close'); }; if (this.res.readable) { this.res.once('end', finish); } else { finish(); } return; } if (!this.destroyed) { this.destroy(new Error('The HTTP/2 stream has been early terminated')); queueMicrotask(() => { this.emit('close'); }); return; } this.destroy(); this.emit('close'); }); for (const job of this.pendingJobs) { job.run(); } this.pendingJobs = []; this.emit('socket', this.socket); } } export const globalAgent = new Http2Agent(); export const request = (input, options, callback) => new Http2ClientRequest(input, options, callback); const getSourceOptions = (input, options) => { if (typeof input === 'object' && !(input instanceof URL)) { return input; } return typeof options === 'object' ? options : {}; }; const requestHttp1 = (options, agent, callback) => { if (typeof agent === 'object' && 'https' in agent) { options.agent = agent.https; } options.ALPNProtocols = ['http/1.1']; delete options.timeout; delete options._socketTimeout; if (options.headers) { const headers = { ...options.headers }; Reflect.deleteProperty(headers, HTTP2_HEADER_METHOD); Reflect.deleteProperty(headers, HTTP2_HEADER_SCHEME); Reflect.deleteProperty(headers, HTTP2_HEADER_PATH); if (headers[HTTP2_HEADER_AUTHORITY] && !headers.host) { headers.host = headers[HTTP2_HEADER_AUTHORITY]; } Reflect.deleteProperty(headers, HTTP2_HEADER_AUTHORITY); options.headers = headers; } return https.request(options, callback); }; const requestWithCustomHttpsAgent = (options, agent, callback) => { options.agent = agent.https; options.ALPNProtocols = ['http/1.1']; delete options.timeout; delete options._socketTimeout; return https.request(options, callback); }; const requestHttp2 = (options, agent, socket, callback) => { options.agent = typeof agent === 'object' && 'http2' in agent ? agent.http2 : agent; if (socket) { options._reuseSocket = socket; options._reuseSocketShouldPool = options.createConnection === undefined; } return request(options, callback); }; export const auto = async (input, options, callback) => { const sourceOptions = getSourceOptions(input, options); const normalized = normalizeInput(input, options, callback); options = normalized.options; callback = normalized.callback; options.ALPNProtocols ??= ['h2', 'http/1.1']; options.protocol ??= 'https:'; if (options.h2session) { return request(options, callback); } const isHttps = options.protocol === 'https:'; if (!isHttps) { options.agent = options.agent?.http; return http.request(options, callback); } const agent = options.agent; if (hasCustomHttpsAgent(agent) && options.createConnection === undefined) { return requestWithCustomHttpsAgent(options, agent, callback); } const { alpnProtocol, socket } = await resolveProtocol(options, sourceOptions); if (alpnProtocol === 'h2') { return requestHttp2(options, agent, socket, callback); } socket?.destroy(); return requestHttp1(options, agent, callback); }; const http2Client = { Agent: Http2Agent, auto, globalAgent, request, }; export default http2Client;