123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- // @ts-check
- 'use strict'
- const assert = require('node:assert')
- const net = require('node:net')
- const http = require('node:http')
- const util = require('../core/util.js')
- const { channels } = require('../core/diagnostics.js')
- const Request = require('../core/request.js')
- const DispatcherBase = require('./dispatcher-base')
- const {
- InvalidArgumentError,
- InformationalError,
- ClientDestroyedError
- } = require('../core/errors.js')
- const buildConnector = require('../core/connect.js')
- const {
- kUrl,
- kServerName,
- kClient,
- kBusy,
- kConnect,
- kResuming,
- kRunning,
- kPending,
- kSize,
- kQueue,
- kConnected,
- kConnecting,
- kNeedDrain,
- kKeepAliveDefaultTimeout,
- kHostHeader,
- kPendingIdx,
- kRunningIdx,
- kError,
- kPipelining,
- kKeepAliveTimeoutValue,
- kMaxHeadersSize,
- kKeepAliveMaxTimeout,
- kKeepAliveTimeoutThreshold,
- kHeadersTimeout,
- kBodyTimeout,
- kStrictContentLength,
- kConnector,
- kMaxRedirections,
- kMaxRequests,
- kCounter,
- kClose,
- kDestroy,
- kDispatch,
- kInterceptors,
- kLocalAddress,
- kMaxResponseSize,
- kOnError,
- kHTTPContext,
- kMaxConcurrentStreams,
- kResume
- } = require('../core/symbols.js')
- const connectH1 = require('./client-h1.js')
- const connectH2 = require('./client-h2.js')
- let deprecatedInterceptorWarned = false
- const kClosedResolve = Symbol('kClosedResolve')
- const noop = () => {}
- function getPipelining (client) {
- return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
- }
- /**
- * @type {import('../../types/client.js').default}
- */
- class Client extends DispatcherBase {
- /**
- *
- * @param {string|URL} url
- * @param {import('../../types/client.js').Client.Options} options
- */
- constructor (url, {
- interceptors,
- maxHeaderSize,
- headersTimeout,
- socketTimeout,
- requestTimeout,
- connectTimeout,
- bodyTimeout,
- idleTimeout,
- keepAlive,
- keepAliveTimeout,
- maxKeepAliveTimeout,
- keepAliveMaxTimeout,
- keepAliveTimeoutThreshold,
- socketPath,
- pipelining,
- tls,
- strictContentLength,
- maxCachedSessions,
- maxRedirections,
- connect,
- maxRequestsPerClient,
- localAddress,
- maxResponseSize,
- autoSelectFamily,
- autoSelectFamilyAttemptTimeout,
- // h2
- maxConcurrentStreams,
- allowH2
- } = {}) {
- super()
- if (keepAlive !== undefined) {
- throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
- }
- if (socketTimeout !== undefined) {
- throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead')
- }
- if (requestTimeout !== undefined) {
- throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead')
- }
- if (idleTimeout !== undefined) {
- throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead')
- }
- if (maxKeepAliveTimeout !== undefined) {
- throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
- }
- if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) {
- throw new InvalidArgumentError('invalid maxHeaderSize')
- }
- if (socketPath != null && typeof socketPath !== 'string') {
- throw new InvalidArgumentError('invalid socketPath')
- }
- if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) {
- throw new InvalidArgumentError('invalid connectTimeout')
- }
- if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) {
- throw new InvalidArgumentError('invalid keepAliveTimeout')
- }
- if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) {
- throw new InvalidArgumentError('invalid keepAliveMaxTimeout')
- }
- if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) {
- throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold')
- }
- if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) {
- throw new InvalidArgumentError('headersTimeout must be a positive integer or zero')
- }
- if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
- throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero')
- }
- if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') {
- throw new InvalidArgumentError('connect must be a function or an object')
- }
- if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) {
- throw new InvalidArgumentError('maxRedirections must be a positive number')
- }
- if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) {
- throw new InvalidArgumentError('maxRequestsPerClient must be a positive number')
- }
- if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) {
- throw new InvalidArgumentError('localAddress must be valid string IP address')
- }
- if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) {
- throw new InvalidArgumentError('maxResponseSize must be a positive number')
- }
- if (
- autoSelectFamilyAttemptTimeout != null &&
- (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)
- ) {
- throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number')
- }
- // h2
- if (allowH2 != null && typeof allowH2 !== 'boolean') {
- throw new InvalidArgumentError('allowH2 must be a valid boolean value')
- }
- if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) {
- throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0')
- }
- if (typeof connect !== 'function') {
- connect = buildConnector({
- ...tls,
- maxCachedSessions,
- allowH2,
- socketPath,
- timeout: connectTimeout,
- ...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined),
- ...connect
- })
- }
- if (interceptors?.Client && Array.isArray(interceptors.Client)) {
- this[kInterceptors] = interceptors.Client
- if (!deprecatedInterceptorWarned) {
- deprecatedInterceptorWarned = true
- process.emitWarning('Client.Options#interceptor is deprecated. Use Dispatcher#compose instead.', {
- code: 'UNDICI-CLIENT-INTERCEPTOR-DEPRECATED'
- })
- }
- } else {
- this[kInterceptors] = [createRedirectInterceptor({ maxRedirections })]
- }
- this[kUrl] = util.parseOrigin(url)
- this[kConnector] = connect
- this[kPipelining] = pipelining != null ? pipelining : 1
- this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize
- this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout
- this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout
- this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold
- this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout]
- this[kServerName] = null
- this[kLocalAddress] = localAddress != null ? localAddress : null
- this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming
- this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming
- this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n`
- this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3
- this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3
- this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength
- this[kMaxRedirections] = maxRedirections
- this[kMaxRequests] = maxRequestsPerClient
- this[kClosedResolve] = null
- this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
- this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
- this[kHTTPContext] = null
- // kQueue is built up of 3 sections separated by
- // the kRunningIdx and kPendingIdx indices.
- // | complete | running | pending |
- // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length
- // kRunningIdx points to the first running element.
- // kPendingIdx points to the first pending element.
- // This implements a fast queue with an amortized
- // time of O(1).
- this[kQueue] = []
- this[kRunningIdx] = 0
- this[kPendingIdx] = 0
- this[kResume] = (sync) => resume(this, sync)
- this[kOnError] = (err) => onError(this, err)
- }
- get pipelining () {
- return this[kPipelining]
- }
- set pipelining (value) {
- this[kPipelining] = value
- this[kResume](true)
- }
- get [kPending] () {
- return this[kQueue].length - this[kPendingIdx]
- }
- get [kRunning] () {
- return this[kPendingIdx] - this[kRunningIdx]
- }
- get [kSize] () {
- return this[kQueue].length - this[kRunningIdx]
- }
- get [kConnected] () {
- return !!this[kHTTPContext] && !this[kConnecting] && !this[kHTTPContext].destroyed
- }
- get [kBusy] () {
- return Boolean(
- this[kHTTPContext]?.busy(null) ||
- (this[kSize] >= (getPipelining(this) || 1)) ||
- this[kPending] > 0
- )
- }
- /* istanbul ignore: only used for test */
- [kConnect] (cb) {
- connect(this)
- this.once('connect', cb)
- }
- [kDispatch] (opts, handler) {
- const origin = opts.origin || this[kUrl].origin
- const request = new Request(origin, opts, handler)
- this[kQueue].push(request)
- if (this[kResuming]) {
- // Do nothing.
- } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) {
- // Wait a tick in case stream/iterator is ended in the same tick.
- this[kResuming] = 1
- queueMicrotask(() => resume(this))
- } else {
- this[kResume](true)
- }
- if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) {
- this[kNeedDrain] = 2
- }
- return this[kNeedDrain] < 2
- }
- async [kClose] () {
- // TODO: for H2 we need to gracefully flush the remaining enqueued
- // request and close each stream.
- return new Promise((resolve) => {
- if (this[kSize]) {
- this[kClosedResolve] = resolve
- } else {
- resolve(null)
- }
- })
- }
- async [kDestroy] (err) {
- return new Promise((resolve) => {
- const requests = this[kQueue].splice(this[kPendingIdx])
- for (let i = 0; i < requests.length; i++) {
- const request = requests[i]
- util.errorRequest(this, request, err)
- }
- const callback = () => {
- if (this[kClosedResolve]) {
- // TODO (fix): Should we error here with ClientDestroyedError?
- this[kClosedResolve]()
- this[kClosedResolve] = null
- }
- resolve(null)
- }
- if (this[kHTTPContext]) {
- this[kHTTPContext].destroy(err, callback)
- this[kHTTPContext] = null
- } else {
- queueMicrotask(callback)
- }
- this[kResume]()
- })
- }
- }
- const createRedirectInterceptor = require('../interceptor/redirect-interceptor.js')
- function onError (client, err) {
- if (
- client[kRunning] === 0 &&
- err.code !== 'UND_ERR_INFO' &&
- err.code !== 'UND_ERR_SOCKET'
- ) {
- // Error is not caused by running request and not a recoverable
- // socket error.
- assert(client[kPendingIdx] === client[kRunningIdx])
- const requests = client[kQueue].splice(client[kRunningIdx])
- for (let i = 0; i < requests.length; i++) {
- const request = requests[i]
- util.errorRequest(client, request, err)
- }
- assert(client[kSize] === 0)
- }
- }
- /**
- * @param {Client} client
- * @returns
- */
- async function connect (client) {
- assert(!client[kConnecting])
- assert(!client[kHTTPContext])
- let { host, hostname, protocol, port } = client[kUrl]
- // Resolve ipv6
- if (hostname[0] === '[') {
- const idx = hostname.indexOf(']')
- assert(idx !== -1)
- const ip = hostname.substring(1, idx)
- assert(net.isIP(ip))
- hostname = ip
- }
- client[kConnecting] = true
- if (channels.beforeConnect.hasSubscribers) {
- channels.beforeConnect.publish({
- connectParams: {
- host,
- hostname,
- protocol,
- port,
- version: client[kHTTPContext]?.version,
- servername: client[kServerName],
- localAddress: client[kLocalAddress]
- },
- connector: client[kConnector]
- })
- }
- try {
- const socket = await new Promise((resolve, reject) => {
- client[kConnector]({
- host,
- hostname,
- protocol,
- port,
- servername: client[kServerName],
- localAddress: client[kLocalAddress]
- }, (err, socket) => {
- if (err) {
- reject(err)
- } else {
- resolve(socket)
- }
- })
- })
- if (client.destroyed) {
- util.destroy(socket.on('error', noop), new ClientDestroyedError())
- return
- }
- assert(socket)
- try {
- client[kHTTPContext] = socket.alpnProtocol === 'h2'
- ? await connectH2(client, socket)
- : await connectH1(client, socket)
- } catch (err) {
- socket.destroy().on('error', noop)
- throw err
- }
- client[kConnecting] = false
- socket[kCounter] = 0
- socket[kMaxRequests] = client[kMaxRequests]
- socket[kClient] = client
- socket[kError] = null
- if (channels.connected.hasSubscribers) {
- channels.connected.publish({
- connectParams: {
- host,
- hostname,
- protocol,
- port,
- version: client[kHTTPContext]?.version,
- servername: client[kServerName],
- localAddress: client[kLocalAddress]
- },
- connector: client[kConnector],
- socket
- })
- }
- client.emit('connect', client[kUrl], [client])
- } catch (err) {
- if (client.destroyed) {
- return
- }
- client[kConnecting] = false
- if (channels.connectError.hasSubscribers) {
- channels.connectError.publish({
- connectParams: {
- host,
- hostname,
- protocol,
- port,
- version: client[kHTTPContext]?.version,
- servername: client[kServerName],
- localAddress: client[kLocalAddress]
- },
- connector: client[kConnector],
- error: err
- })
- }
- if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
- assert(client[kRunning] === 0)
- while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
- const request = client[kQueue][client[kPendingIdx]++]
- util.errorRequest(client, request, err)
- }
- } else {
- onError(client, err)
- }
- client.emit('connectionError', client[kUrl], [client], err)
- }
- client[kResume]()
- }
- function emitDrain (client) {
- client[kNeedDrain] = 0
- client.emit('drain', client[kUrl], [client])
- }
- function resume (client, sync) {
- if (client[kResuming] === 2) {
- return
- }
- client[kResuming] = 2
- _resume(client, sync)
- client[kResuming] = 0
- if (client[kRunningIdx] > 256) {
- client[kQueue].splice(0, client[kRunningIdx])
- client[kPendingIdx] -= client[kRunningIdx]
- client[kRunningIdx] = 0
- }
- }
- function _resume (client, sync) {
- while (true) {
- if (client.destroyed) {
- assert(client[kPending] === 0)
- return
- }
- if (client[kClosedResolve] && !client[kSize]) {
- client[kClosedResolve]()
- client[kClosedResolve] = null
- return
- }
- if (client[kHTTPContext]) {
- client[kHTTPContext].resume()
- }
- if (client[kBusy]) {
- client[kNeedDrain] = 2
- } else if (client[kNeedDrain] === 2) {
- if (sync) {
- client[kNeedDrain] = 1
- queueMicrotask(() => emitDrain(client))
- } else {
- emitDrain(client)
- }
- continue
- }
- if (client[kPending] === 0) {
- return
- }
- if (client[kRunning] >= (getPipelining(client) || 1)) {
- return
- }
- const request = client[kQueue][client[kPendingIdx]]
- if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) {
- if (client[kRunning] > 0) {
- return
- }
- client[kServerName] = request.servername
- client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => {
- client[kHTTPContext] = null
- resume(client)
- })
- }
- if (client[kConnecting]) {
- return
- }
- if (!client[kHTTPContext]) {
- connect(client)
- return
- }
- if (client[kHTTPContext].destroyed) {
- return
- }
- if (client[kHTTPContext].busy(request)) {
- return
- }
- if (!request.aborted && client[kHTTPContext].write(request)) {
- client[kPendingIdx]++
- } else {
- client[kQueue].splice(client[kPendingIdx], 1)
- }
- }
- }
- module.exports = Client
|