aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/node_modules/undici/lib/core
diff options
context:
space:
mode:
authorEgor Tensin <egor@tensin.name>2026-02-28 21:59:12 +0100
committerEgor Tensin <egor@tensin.name>2026-02-28 22:19:57 +0100
commit9611c248995ad3bacda1d0a93d0b5e1e6605d162 (patch)
treeeb3ae35b426e9d72e7cb34e8e9cc0453745a8e17 /node_modules/undici/lib/core
parentDEVELOPMENT.md: expand on sources (diff)
downloadcleanup-path-9611c248995ad3bacda1d0a93d0b5e1e6605d162.tar.gz
cleanup-path-9611c248995ad3bacda1d0a93d0b5e1e6605d162.zip
upgrade actions/core to version 2
Diffstat (limited to 'node_modules/undici/lib/core')
-rw-r--r--node_modules/undici/lib/core/connect.js119
-rw-r--r--node_modules/undici/lib/core/diagnostics.js202
-rw-r--r--node_modules/undici/lib/core/errors.js218
-rw-r--r--node_modules/undici/lib/core/request.js292
-rw-r--r--node_modules/undici/lib/core/symbols.js16
-rw-r--r--node_modules/undici/lib/core/tree.js152
-rw-r--r--node_modules/undici/lib/core/util.js433
7 files changed, 1055 insertions, 377 deletions
diff --git a/node_modules/undici/lib/core/connect.js b/node_modules/undici/lib/core/connect.js
index 3309117..8cd8abc 100644
--- a/node_modules/undici/lib/core/connect.js
+++ b/node_modules/undici/lib/core/connect.js
@@ -1,9 +1,12 @@
'use strict'
-const net = require('net')
-const assert = require('assert')
+const net = require('node:net')
+const assert = require('node:assert')
const util = require('./util')
const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
+const timers = require('../util/timers')
+
+function noop () {}
let tls // include tls conditionally since it is not always available
@@ -15,7 +18,7 @@ let tls // include tls conditionally since it is not always available
let SessionCache
// FIXME: remove workaround when the Node bug is fixed
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
-if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) {
+if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
SessionCache = class WeakSessionCache {
constructor (maxCachedSessions) {
this._maxCachedSessions = maxCachedSessions
@@ -73,7 +76,7 @@ if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) {
}
}
-function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) {
+function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
}
@@ -86,15 +89,17 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...o
let socket
if (protocol === 'https:') {
if (!tls) {
- tls = require('tls')
+ tls = require('node:tls')
}
servername = servername || options.servername || util.getServerName(host) || null
const sessionKey = servername || hostname
- const session = sessionCache.get(sessionKey) || null
-
assert(sessionKey)
+ const session = customSession || sessionCache.get(sessionKey) || null
+
+ port = port || 443
+
socket = tls.connect({
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
...options,
@@ -104,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...o
// TODO(HTTP/2): Add support for h2c
ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
socket: httpSocket, // upgrade socket connection
- port: port || 443,
+ port,
host: hostname
})
@@ -115,11 +120,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...o
})
} else {
assert(!httpSocket, 'httpSocket can only be sent on TLS update')
+
+ port = port || 80
+
socket = net.connect({
highWaterMark: 64 * 1024, // Same as nodejs fs streams.
...options,
localAddress,
- port: port || 80,
+ port,
host: hostname
})
}
@@ -130,12 +138,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...o
socket.setKeepAlive(true, keepAliveInitialDelay)
}
- const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
+ const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
socket
.setNoDelay(true)
.once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
- cancelTimeout()
+ queueMicrotask(clearConnectTimeout)
if (callback) {
const cb = callback
@@ -144,7 +152,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...o
}
})
.on('error', function (err) {
- cancelTimeout()
+ queueMicrotask(clearConnectTimeout)
if (callback) {
const cb = callback
@@ -157,33 +165,76 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...o
}
}
-function setupTimeout (onConnectTimeout, timeout) {
- if (!timeout) {
- return () => {}
- }
+/**
+ * @param {WeakRef<net.Socket>} socketWeakRef
+ * @param {object} opts
+ * @param {number} opts.timeout
+ * @param {string} opts.hostname
+ * @param {number} opts.port
+ * @returns {() => void}
+ */
+const setupConnectTimeout = process.platform === 'win32'
+ ? (socketWeakRef, opts) => {
+ if (!opts.timeout) {
+ return noop
+ }
- let s1 = null
- let s2 = null
- const timeoutId = setTimeout(() => {
- // setImmediate is added to make sure that we priotorise socket error events over timeouts
- s1 = setImmediate(() => {
- if (process.platform === 'win32') {
+ let s1 = null
+ let s2 = null
+ const fastTimer = timers.setFastTimeout(() => {
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
+ s1 = setImmediate(() => {
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
- s2 = setImmediate(() => onConnectTimeout())
- } else {
- onConnectTimeout()
+ s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
+ })
+ }, opts.timeout)
+ return () => {
+ timers.clearFastTimeout(fastTimer)
+ clearImmediate(s1)
+ clearImmediate(s2)
+ }
+ }
+ : (socketWeakRef, opts) => {
+ if (!opts.timeout) {
+ return noop
}
- })
- }, timeout)
- return () => {
- clearTimeout(timeoutId)
- clearImmediate(s1)
- clearImmediate(s2)
+
+ let s1 = null
+ const fastTimer = timers.setFastTimeout(() => {
+ // setImmediate is added to make sure that we prioritize socket error events over timeouts
+ s1 = setImmediate(() => {
+ onConnectTimeout(socketWeakRef.deref(), opts)
+ })
+ }, opts.timeout)
+ return () => {
+ timers.clearFastTimeout(fastTimer)
+ clearImmediate(s1)
+ }
+ }
+
+/**
+ * @param {net.Socket} socket
+ * @param {object} opts
+ * @param {number} opts.timeout
+ * @param {string} opts.hostname
+ * @param {number} opts.port
+ */
+function onConnectTimeout (socket, opts) {
+ // The socket could be already garbage collected
+ if (socket == null) {
+ return
}
-}
-function onConnectTimeout (socket) {
- util.destroy(socket, new ConnectTimeoutError())
+ let message = 'Connect Timeout Error'
+ if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
+ message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
+ } else {
+ message += ` (attempted address: ${opts.hostname}:${opts.port},`
+ }
+
+ message += ` timeout: ${opts.timeout}ms)`
+
+ util.destroy(socket, new ConnectTimeoutError(message))
}
module.exports = buildConnector
diff --git a/node_modules/undici/lib/core/diagnostics.js b/node_modules/undici/lib/core/diagnostics.js
new file mode 100644
index 0000000..e1af3db
--- /dev/null
+++ b/node_modules/undici/lib/core/diagnostics.js
@@ -0,0 +1,202 @@
+'use strict'
+const diagnosticsChannel = require('node:diagnostics_channel')
+const util = require('node:util')
+
+const undiciDebugLog = util.debuglog('undici')
+const fetchDebuglog = util.debuglog('fetch')
+const websocketDebuglog = util.debuglog('websocket')
+let isClientSet = false
+const channels = {
+ // Client
+ beforeConnect: diagnosticsChannel.channel('undici:client:beforeConnect'),
+ connected: diagnosticsChannel.channel('undici:client:connected'),
+ connectError: diagnosticsChannel.channel('undici:client:connectError'),
+ sendHeaders: diagnosticsChannel.channel('undici:client:sendHeaders'),
+ // Request
+ create: diagnosticsChannel.channel('undici:request:create'),
+ bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
+ headers: diagnosticsChannel.channel('undici:request:headers'),
+ trailers: diagnosticsChannel.channel('undici:request:trailers'),
+ error: diagnosticsChannel.channel('undici:request:error'),
+ // WebSocket
+ open: diagnosticsChannel.channel('undici:websocket:open'),
+ close: diagnosticsChannel.channel('undici:websocket:close'),
+ socketError: diagnosticsChannel.channel('undici:websocket:socket_error'),
+ ping: diagnosticsChannel.channel('undici:websocket:ping'),
+ pong: diagnosticsChannel.channel('undici:websocket:pong')
+}
+
+if (undiciDebugLog.enabled || fetchDebuglog.enabled) {
+ const debuglog = fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog
+
+ // Track all Client events
+ diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(evt => {
+ const {
+ connectParams: { version, protocol, port, host }
+ } = evt
+ debuglog(
+ 'connecting to %s using %s%s',
+ `${host}${port ? `:${port}` : ''}`,
+ protocol,
+ version
+ )
+ })
+
+ diagnosticsChannel.channel('undici:client:connected').subscribe(evt => {
+ const {
+ connectParams: { version, protocol, port, host }
+ } = evt
+ debuglog(
+ 'connected to %s using %s%s',
+ `${host}${port ? `:${port}` : ''}`,
+ protocol,
+ version
+ )
+ })
+
+ diagnosticsChannel.channel('undici:client:connectError').subscribe(evt => {
+ const {
+ connectParams: { version, protocol, port, host },
+ error
+ } = evt
+ debuglog(
+ 'connection to %s using %s%s errored - %s',
+ `${host}${port ? `:${port}` : ''}`,
+ protocol,
+ version,
+ error.message
+ )
+ })
+
+ diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(evt => {
+ const {
+ request: { method, path, origin }
+ } = evt
+ debuglog('sending request to %s %s/%s', method, origin, path)
+ })
+
+ // Track Request events
+ diagnosticsChannel.channel('undici:request:headers').subscribe(evt => {
+ const {
+ request: { method, path, origin },
+ response: { statusCode }
+ } = evt
+ debuglog(
+ 'received response to %s %s/%s - HTTP %d',
+ method,
+ origin,
+ path,
+ statusCode
+ )
+ })
+
+ diagnosticsChannel.channel('undici:request:trailers').subscribe(evt => {
+ const {
+ request: { method, path, origin }
+ } = evt
+ debuglog('trailers received from %s %s/%s', method, origin, path)
+ })
+
+ diagnosticsChannel.channel('undici:request:error').subscribe(evt => {
+ const {
+ request: { method, path, origin },
+ error
+ } = evt
+ debuglog(
+ 'request to %s %s/%s errored - %s',
+ method,
+ origin,
+ path,
+ error.message
+ )
+ })
+
+ isClientSet = true
+}
+
+if (websocketDebuglog.enabled) {
+ if (!isClientSet) {
+ const debuglog = undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog
+ diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(evt => {
+ const {
+ connectParams: { version, protocol, port, host }
+ } = evt
+ debuglog(
+ 'connecting to %s%s using %s%s',
+ host,
+ port ? `:${port}` : '',
+ protocol,
+ version
+ )
+ })
+
+ diagnosticsChannel.channel('undici:client:connected').subscribe(evt => {
+ const {
+ connectParams: { version, protocol, port, host }
+ } = evt
+ debuglog(
+ 'connected to %s%s using %s%s',
+ host,
+ port ? `:${port}` : '',
+ protocol,
+ version
+ )
+ })
+
+ diagnosticsChannel.channel('undici:client:connectError').subscribe(evt => {
+ const {
+ connectParams: { version, protocol, port, host },
+ error
+ } = evt
+ debuglog(
+ 'connection to %s%s using %s%s errored - %s',
+ host,
+ port ? `:${port}` : '',
+ protocol,
+ version,
+ error.message
+ )
+ })
+
+ diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(evt => {
+ const {
+ request: { method, path, origin }
+ } = evt
+ debuglog('sending request to %s %s/%s', method, origin, path)
+ })
+ }
+
+ // Track all WebSocket events
+ diagnosticsChannel.channel('undici:websocket:open').subscribe(evt => {
+ const {
+ address: { address, port }
+ } = evt
+ websocketDebuglog('connection opened %s%s', address, port ? `:${port}` : '')
+ })
+
+ diagnosticsChannel.channel('undici:websocket:close').subscribe(evt => {
+ const { websocket, code, reason } = evt
+ websocketDebuglog(
+ 'closed connection to %s - %s %s',
+ websocket.url,
+ code,
+ reason
+ )
+ })
+
+ diagnosticsChannel.channel('undici:websocket:socket_error').subscribe(err => {
+ websocketDebuglog('connection errored - %s', err.message)
+ })
+
+ diagnosticsChannel.channel('undici:websocket:ping').subscribe(evt => {
+ websocketDebuglog('ping received')
+ })
+
+ diagnosticsChannel.channel('undici:websocket:pong').subscribe(evt => {
+ websocketDebuglog('pong received')
+ })
+}
+
+module.exports = {
+ channels
+}
diff --git a/node_modules/undici/lib/core/errors.js b/node_modules/undici/lib/core/errors.js
index 7af704b..535c733 100644
--- a/node_modules/undici/lib/core/errors.js
+++ b/node_modules/undici/lib/core/errors.js
@@ -1,57 +1,88 @@
'use strict'
+const kUndiciError = Symbol.for('undici.error.UND_ERR')
class UndiciError extends Error {
constructor (message) {
super(message)
this.name = 'UndiciError'
this.code = 'UND_ERR'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kUndiciError] === true
+ }
+
+ [kUndiciError] = true
}
+const kConnectTimeoutError = Symbol.for('undici.error.UND_ERR_CONNECT_TIMEOUT')
class ConnectTimeoutError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, ConnectTimeoutError)
this.name = 'ConnectTimeoutError'
this.message = message || 'Connect Timeout Error'
this.code = 'UND_ERR_CONNECT_TIMEOUT'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kConnectTimeoutError] === true
+ }
+
+ [kConnectTimeoutError] = true
}
+const kHeadersTimeoutError = Symbol.for('undici.error.UND_ERR_HEADERS_TIMEOUT')
class HeadersTimeoutError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, HeadersTimeoutError)
this.name = 'HeadersTimeoutError'
this.message = message || 'Headers Timeout Error'
this.code = 'UND_ERR_HEADERS_TIMEOUT'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kHeadersTimeoutError] === true
+ }
+
+ [kHeadersTimeoutError] = true
}
+const kHeadersOverflowError = Symbol.for('undici.error.UND_ERR_HEADERS_OVERFLOW')
class HeadersOverflowError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, HeadersOverflowError)
this.name = 'HeadersOverflowError'
this.message = message || 'Headers Overflow Error'
this.code = 'UND_ERR_HEADERS_OVERFLOW'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kHeadersOverflowError] === true
+ }
+
+ [kHeadersOverflowError] = true
}
+const kBodyTimeoutError = Symbol.for('undici.error.UND_ERR_BODY_TIMEOUT')
class BodyTimeoutError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, BodyTimeoutError)
this.name = 'BodyTimeoutError'
this.message = message || 'Body Timeout Error'
this.code = 'UND_ERR_BODY_TIMEOUT'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kBodyTimeoutError] === true
+ }
+
+ [kBodyTimeoutError] = true
}
+const kResponseStatusCodeError = Symbol.for('undici.error.UND_ERR_RESPONSE_STATUS_CODE')
class ResponseStatusCodeError extends UndiciError {
constructor (message, statusCode, headers, body) {
super(message)
- Error.captureStackTrace(this, ResponseStatusCodeError)
this.name = 'ResponseStatusCodeError'
this.message = message || 'Response Status Code Error'
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
@@ -60,143 +91,243 @@ class ResponseStatusCodeError extends UndiciError {
this.statusCode = statusCode
this.headers = headers
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseStatusCodeError] === true
+ }
+
+ [kResponseStatusCodeError] = true
}
+const kInvalidArgumentError = Symbol.for('undici.error.UND_ERR_INVALID_ARG')
class InvalidArgumentError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, InvalidArgumentError)
this.name = 'InvalidArgumentError'
this.message = message || 'Invalid Argument Error'
this.code = 'UND_ERR_INVALID_ARG'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kInvalidArgumentError] === true
+ }
+
+ [kInvalidArgumentError] = true
}
+const kInvalidReturnValueError = Symbol.for('undici.error.UND_ERR_INVALID_RETURN_VALUE')
class InvalidReturnValueError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, InvalidReturnValueError)
this.name = 'InvalidReturnValueError'
this.message = message || 'Invalid Return Value Error'
this.code = 'UND_ERR_INVALID_RETURN_VALUE'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kInvalidReturnValueError] === true
+ }
+
+ [kInvalidReturnValueError] = true
+}
+
+const kAbortError = Symbol.for('undici.error.UND_ERR_ABORT')
+class AbortError extends UndiciError {
+ constructor (message) {
+ super(message)
+ this.name = 'AbortError'
+ this.message = message || 'The operation was aborted'
+ this.code = 'UND_ERR_ABORT'
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kAbortError] === true
+ }
+
+ [kAbortError] = true
}
-class RequestAbortedError extends UndiciError {
+const kRequestAbortedError = Symbol.for('undici.error.UND_ERR_ABORTED')
+class RequestAbortedError extends AbortError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, RequestAbortedError)
this.name = 'AbortError'
this.message = message || 'Request aborted'
this.code = 'UND_ERR_ABORTED'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kRequestAbortedError] === true
+ }
+
+ [kRequestAbortedError] = true
}
+const kInformationalError = Symbol.for('undici.error.UND_ERR_INFO')
class InformationalError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, InformationalError)
this.name = 'InformationalError'
this.message = message || 'Request information'
this.code = 'UND_ERR_INFO'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kInformationalError] === true
+ }
+
+ [kInformationalError] = true
}
+const kRequestContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH')
class RequestContentLengthMismatchError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, RequestContentLengthMismatchError)
this.name = 'RequestContentLengthMismatchError'
this.message = message || 'Request body length does not match content-length header'
this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kRequestContentLengthMismatchError] === true
+ }
+
+ [kRequestContentLengthMismatchError] = true
}
+const kResponseContentLengthMismatchError = Symbol.for('undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH')
class ResponseContentLengthMismatchError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, ResponseContentLengthMismatchError)
this.name = 'ResponseContentLengthMismatchError'
this.message = message || 'Response body length does not match content-length header'
this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseContentLengthMismatchError] === true
+ }
+
+ [kResponseContentLengthMismatchError] = true
}
+const kClientDestroyedError = Symbol.for('undici.error.UND_ERR_DESTROYED')
class ClientDestroyedError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, ClientDestroyedError)
this.name = 'ClientDestroyedError'
this.message = message || 'The client is destroyed'
this.code = 'UND_ERR_DESTROYED'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kClientDestroyedError] === true
+ }
+
+ [kClientDestroyedError] = true
}
+const kClientClosedError = Symbol.for('undici.error.UND_ERR_CLOSED')
class ClientClosedError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, ClientClosedError)
this.name = 'ClientClosedError'
this.message = message || 'The client is closed'
this.code = 'UND_ERR_CLOSED'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kClientClosedError] === true
+ }
+
+ [kClientClosedError] = true
}
+const kSocketError = Symbol.for('undici.error.UND_ERR_SOCKET')
class SocketError extends UndiciError {
constructor (message, socket) {
super(message)
- Error.captureStackTrace(this, SocketError)
this.name = 'SocketError'
this.message = message || 'Socket error'
this.code = 'UND_ERR_SOCKET'
this.socket = socket
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kSocketError] === true
+ }
+
+ [kSocketError] = true
}
+const kNotSupportedError = Symbol.for('undici.error.UND_ERR_NOT_SUPPORTED')
class NotSupportedError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, NotSupportedError)
this.name = 'NotSupportedError'
this.message = message || 'Not supported error'
this.code = 'UND_ERR_NOT_SUPPORTED'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kNotSupportedError] === true
+ }
+
+ [kNotSupportedError] = true
}
+const kBalancedPoolMissingUpstreamError = Symbol.for('undici.error.UND_ERR_BPL_MISSING_UPSTREAM')
class BalancedPoolMissingUpstreamError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, NotSupportedError)
this.name = 'MissingUpstreamError'
this.message = message || 'No upstream has been added to the BalancedPool'
this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kBalancedPoolMissingUpstreamError] === true
+ }
+
+ [kBalancedPoolMissingUpstreamError] = true
}
+const kHTTPParserError = Symbol.for('undici.error.UND_ERR_HTTP_PARSER')
class HTTPParserError extends Error {
constructor (message, code, data) {
super(message)
- Error.captureStackTrace(this, HTTPParserError)
this.name = 'HTTPParserError'
this.code = code ? `HPE_${code}` : undefined
this.data = data ? data.toString() : undefined
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kHTTPParserError] === true
+ }
+
+ [kHTTPParserError] = true
}
+const kResponseExceededMaxSizeError = Symbol.for('undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE')
class ResponseExceededMaxSizeError extends UndiciError {
constructor (message) {
super(message)
- Error.captureStackTrace(this, ResponseExceededMaxSizeError)
this.name = 'ResponseExceededMaxSizeError'
this.message = message || 'Response content exceeded max size'
this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseExceededMaxSizeError] === true
+ }
+
+ [kResponseExceededMaxSizeError] = true
}
+const kRequestRetryError = Symbol.for('undici.error.UND_ERR_REQ_RETRY')
class RequestRetryError extends UndiciError {
constructor (message, code, { headers, data }) {
super(message)
- Error.captureStackTrace(this, RequestRetryError)
this.name = 'RequestRetryError'
this.message = message || 'Request retry error'
this.code = 'UND_ERR_REQ_RETRY'
@@ -204,9 +335,52 @@ class RequestRetryError extends UndiciError {
this.data = data
this.headers = headers
}
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kRequestRetryError] === true
+ }
+
+ [kRequestRetryError] = true
+}
+
+const kResponseError = Symbol.for('undici.error.UND_ERR_RESPONSE')
+class ResponseError extends UndiciError {
+ constructor (message, code, { headers, data }) {
+ super(message)
+ this.name = 'ResponseError'
+ this.message = message || 'Response error'
+ this.code = 'UND_ERR_RESPONSE'
+ this.statusCode = code
+ this.data = data
+ this.headers = headers
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kResponseError] === true
+ }
+
+ [kResponseError] = true
+}
+
+const kSecureProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_TLS')
+class SecureProxyConnectionError extends UndiciError {
+ constructor (cause, message, options) {
+ super(message, { cause, ...(options ?? {}) })
+ this.name = 'SecureProxyConnectionError'
+ this.message = message || 'Secure Proxy Connection failed'
+ this.code = 'UND_ERR_PRX_TLS'
+ this.cause = cause
+ }
+
+ static [Symbol.hasInstance] (instance) {
+ return instance && instance[kSecureProxyConnectionError] === true
+ }
+
+ [kSecureProxyConnectionError] = true
}
module.exports = {
+ AbortError,
HTTPParserError,
UndiciError,
HeadersTimeoutError,
@@ -226,5 +400,7 @@ module.exports = {
ResponseContentLengthMismatchError,
BalancedPoolMissingUpstreamError,
ResponseExceededMaxSizeError,
- RequestRetryError
+ RequestRetryError,
+ ResponseError,
+ SecureProxyConnectionError
}
diff --git a/node_modules/undici/lib/core/request.js b/node_modules/undici/lib/core/request.js
index 3697e6a..7800303 100644
--- a/node_modules/undici/lib/core/request.js
+++ b/node_modules/undici/lib/core/request.js
@@ -4,52 +4,29 @@ const {
InvalidArgumentError,
NotSupportedError
} = require('./errors')
-const assert = require('assert')
-const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
-const util = require('./util')
-
-// tokenRegExp and headerCharRegex have been lifted from
-// https://github.com/nodejs/node/blob/main/lib/_http_common.js
-
-/**
- * Verifies that the given val is a valid HTTP token
- * per the rules defined in RFC 7230
- * See https://tools.ietf.org/html/rfc7230#section-3.2.6
- */
-const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
-
-/**
- * Matches if val contains an invalid field-vchar
- * field-value = *( field-content / obs-fold )
- * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
- * field-vchar = VCHAR / obs-text
- */
-const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
+const assert = require('node:assert')
+const {
+ isValidHTTPToken,
+ isValidHeaderValue,
+ isStream,
+ destroy,
+ isBuffer,
+ isFormDataLike,
+ isIterable,
+ isBlobLike,
+ buildURL,
+ validateHandler,
+ getServerName,
+ normalizedMethodRecords
+} = require('./util')
+const { channels } = require('./diagnostics.js')
+const { headerNameLowerCasedRecord } = require('./constants')
// Verifies that a given path is valid does not contain control chars \x00 to \x20
const invalidPathRegex = /[^\u0021-\u00ff]/
const kHandler = Symbol('handler')
-const channels = {}
-
-let extractBody
-
-try {
- const diagnosticsChannel = require('diagnostics_channel')
- channels.create = diagnosticsChannel.channel('undici:request:create')
- channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
- channels.headers = diagnosticsChannel.channel('undici:request:headers')
- channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
- channels.error = diagnosticsChannel.channel('undici:request:error')
-} catch {
- channels.create = { hasSubscribers: false }
- channels.bodySent = { hasSubscribers: false }
- channels.headers = { hasSubscribers: false }
- channels.trailers = { hasSubscribers: false }
- channels.error = { hasSubscribers: false }
-}
-
class Request {
constructor (origin, {
path,
@@ -64,7 +41,8 @@ class Request {
bodyTimeout,
reset,
throwOnError,
- expectContinue
+ expectContinue,
+ servername
}, handler) {
if (typeof path !== 'string') {
throw new InvalidArgumentError('path must be a string')
@@ -74,13 +52,13 @@ class Request {
method !== 'CONNECT'
) {
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
- } else if (invalidPathRegex.exec(path) !== null) {
+ } else if (invalidPathRegex.test(path)) {
throw new InvalidArgumentError('invalid request path')
}
if (typeof method !== 'string') {
throw new InvalidArgumentError('method must be a string')
- } else if (tokenRegExp.exec(method) === null) {
+ } else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) {
throw new InvalidArgumentError('invalid request method')
}
@@ -116,13 +94,13 @@ class Request {
if (body == null) {
this.body = null
- } else if (util.isStream(body)) {
+ } else if (isStream(body)) {
this.body = body
const rState = this.body._readableState
if (!rState || !rState.autoDestroy) {
this.endHandler = function autoDestroy () {
- util.destroy(this)
+ destroy(this)
}
this.body.on('end', this.endHandler)
}
@@ -135,7 +113,7 @@ class Request {
}
}
this.body.on('error', this.errorHandler)
- } else if (util.isBuffer(body)) {
+ } else if (isBuffer(body)) {
this.body = body.byteLength ? body : null
} else if (ArrayBuffer.isView(body)) {
this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
@@ -143,7 +121,7 @@ class Request {
this.body = body.byteLength ? Buffer.from(body) : null
} else if (typeof body === 'string') {
this.body = body.length ? Buffer.from(body) : null
- } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
+ } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) {
this.body = body
} else {
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
@@ -155,7 +133,7 @@ class Request {
this.upgrade = upgrade || null
- this.path = query ? util.buildURL(path, query) : path
+ this.path = query ? buildURL(path, query) : path
this.origin = origin
@@ -173,7 +151,7 @@ class Request {
this.contentType = null
- this.headers = ''
+ this.headers = []
// Only for H2
this.expectContinue = expectContinue != null ? expectContinue : false
@@ -186,39 +164,26 @@ class Request {
processHeader(this, headers[i], headers[i + 1])
}
} else if (headers && typeof headers === 'object') {
- const keys = Object.keys(headers)
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i]
- processHeader(this, key, headers[key])
+ if (headers[Symbol.iterator]) {
+ for (const header of headers) {
+ if (!Array.isArray(header) || header.length !== 2) {
+ throw new InvalidArgumentError('headers must be in key-value pair format')
+ }
+ processHeader(this, header[0], header[1])
+ }
+ } else {
+ const keys = Object.keys(headers)
+ for (let i = 0; i < keys.length; ++i) {
+ processHeader(this, keys[i], headers[keys[i]])
+ }
}
} else if (headers != null) {
throw new InvalidArgumentError('headers must be an object or an array')
}
- if (util.isFormDataLike(this.body)) {
- if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
- throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
- }
+ validateHandler(handler, method, upgrade)
- if (!extractBody) {
- extractBody = require('../fetch/body.js').extractBody
- }
-
- const [bodyStream, contentType] = extractBody(body)
- if (this.contentType == null) {
- this.contentType = contentType
- this.headers += `content-type: ${contentType}\r\n`
- }
- this.body = bodyStream.stream
- this.contentLength = bodyStream.length
- } else if (util.isBlobLike(body) && this.contentType == null && body.type) {
- this.contentType = body.type
- this.headers += `content-type: ${body.type}\r\n`
- }
-
- util.validateHandler(handler, method, upgrade)
-
- this.servername = util.getServerName(this.host)
+ this.servername = servername || getServerName(this.host)
this[kHandler] = handler
@@ -263,6 +228,10 @@ class Request {
}
}
+ onResponseStarted () {
+ return this[kHandler].onResponseStarted?.()
+ }
+
onHeaders (statusCode, headers, resume, statusText) {
assert(!this.aborted)
assert(!this.completed)
@@ -342,157 +311,84 @@ class Request {
}
}
- // TODO: adjust to support H2
addHeader (key, value) {
processHeader(this, key, value)
return this
}
-
- static [kHTTP1BuildRequest] (origin, opts, handler) {
- // TODO: Migrate header parsing here, to make Requests
- // HTTP agnostic
- return new Request(origin, opts, handler)
- }
-
- static [kHTTP2BuildRequest] (origin, opts, handler) {
- const headers = opts.headers
- opts = { ...opts, headers: null }
-
- const request = new Request(origin, opts, handler)
-
- request.headers = {}
-
- if (Array.isArray(headers)) {
- if (headers.length % 2 !== 0) {
- throw new InvalidArgumentError('headers array must be even')
- }
- for (let i = 0; i < headers.length; i += 2) {
- processHeader(request, headers[i], headers[i + 1], true)
- }
- } else if (headers && typeof headers === 'object') {
- const keys = Object.keys(headers)
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i]
- processHeader(request, key, headers[key], true)
- }
- } else if (headers != null) {
- throw new InvalidArgumentError('headers must be an object or an array')
- }
-
- return request
- }
-
- static [kHTTP2CopyHeaders] (raw) {
- const rawHeaders = raw.split('\r\n')
- const headers = {}
-
- for (const header of rawHeaders) {
- const [key, value] = header.split(': ')
-
- if (value == null || value.length === 0) continue
-
- if (headers[key]) headers[key] += `,${value}`
- else headers[key] = value
- }
-
- return headers
- }
}
-function processHeaderValue (key, val, skipAppend) {
- if (val && typeof val === 'object') {
+function processHeader (request, key, val) {
+ if (val && (typeof val === 'object' && !Array.isArray(val))) {
throw new InvalidArgumentError(`invalid ${key} header`)
+ } else if (val === undefined) {
+ return
}
- val = val != null ? `${val}` : ''
+ let headerName = headerNameLowerCasedRecord[key]
- if (headerCharRegex.exec(val) !== null) {
- throw new InvalidArgumentError(`invalid ${key} header`)
+ if (headerName === undefined) {
+ headerName = key.toLowerCase()
+ if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) {
+ throw new InvalidArgumentError('invalid header key')
+ }
}
- return skipAppend ? val : `${key}: ${val}\r\n`
-}
-
-function processHeader (request, key, val, skipAppend = false) {
- if (val && (typeof val === 'object' && !Array.isArray(val))) {
- throw new InvalidArgumentError(`invalid ${key} header`)
- } else if (val === undefined) {
- return
+ if (Array.isArray(val)) {
+ const arr = []
+ for (let i = 0; i < val.length; i++) {
+ if (typeof val[i] === 'string') {
+ if (!isValidHeaderValue(val[i])) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+ arr.push(val[i])
+ } else if (val[i] === null) {
+ arr.push('')
+ } else if (typeof val[i] === 'object') {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ } else {
+ arr.push(`${val[i]}`)
+ }
+ }
+ val = arr
+ } else if (typeof val === 'string') {
+ if (!isValidHeaderValue(val)) {
+ throw new InvalidArgumentError(`invalid ${key} header`)
+ }
+ } else if (val === null) {
+ val = ''
+ } else {
+ val = `${val}`
}
- if (
- request.host === null &&
- key.length === 4 &&
- key.toLowerCase() === 'host'
- ) {
- if (headerCharRegex.exec(val) !== null) {
- throw new InvalidArgumentError(`invalid ${key} header`)
+ if (request.host === null && headerName === 'host') {
+ if (typeof val !== 'string') {
+ throw new InvalidArgumentError('invalid host header')
}
// Consumed by Client
request.host = val
- } else if (
- request.contentLength === null &&
- key.length === 14 &&
- key.toLowerCase() === 'content-length'
- ) {
+ } else if (request.contentLength === null && headerName === 'content-length') {
request.contentLength = parseInt(val, 10)
if (!Number.isFinite(request.contentLength)) {
throw new InvalidArgumentError('invalid content-length header')
}
- } else if (
- request.contentType === null &&
- key.length === 12 &&
- key.toLowerCase() === 'content-type'
- ) {
+ } else if (request.contentType === null && headerName === 'content-type') {
request.contentType = val
- if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
- else request.headers += processHeaderValue(key, val)
- } else if (
- key.length === 17 &&
- key.toLowerCase() === 'transfer-encoding'
- ) {
- throw new InvalidArgumentError('invalid transfer-encoding header')
- } else if (
- key.length === 10 &&
- key.toLowerCase() === 'connection'
- ) {
+ request.headers.push(key, val)
+ } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
+ throw new InvalidArgumentError(`invalid ${headerName} header`)
+ } else if (headerName === 'connection') {
const value = typeof val === 'string' ? val.toLowerCase() : null
if (value !== 'close' && value !== 'keep-alive') {
throw new InvalidArgumentError('invalid connection header')
- } else if (value === 'close') {
+ }
+
+ if (value === 'close') {
request.reset = true
}
- } else if (
- key.length === 10 &&
- key.toLowerCase() === 'keep-alive'
- ) {
- throw new InvalidArgumentError('invalid keep-alive header')
- } else if (
- key.length === 7 &&
- key.toLowerCase() === 'upgrade'
- ) {
- throw new InvalidArgumentError('invalid upgrade header')
- } else if (
- key.length === 6 &&
- key.toLowerCase() === 'expect'
- ) {
+ } else if (headerName === 'expect') {
throw new NotSupportedError('expect header not supported')
- } else if (tokenRegExp.exec(key) === null) {
- throw new InvalidArgumentError('invalid header key')
} else {
- if (Array.isArray(val)) {
- for (let i = 0; i < val.length; i++) {
- if (skipAppend) {
- if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}`
- else request.headers[key] = processHeaderValue(key, val[i], skipAppend)
- } else {
- request.headers += processHeaderValue(key, val[i])
- }
- }
- } else {
- if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
- else request.headers += processHeaderValue(key, val)
- }
+ request.headers.push(key, val)
}
}
diff --git a/node_modules/undici/lib/core/symbols.js b/node_modules/undici/lib/core/symbols.js
index 68d8566..c8ba5dd 100644
--- a/node_modules/undici/lib/core/symbols.js
+++ b/node_modules/undici/lib/core/symbols.js
@@ -8,7 +8,6 @@ module.exports = {
kQueue: Symbol('queue'),
kConnect: Symbol('connect'),
kConnecting: Symbol('connecting'),
- kHeadersList: Symbol('headers list'),
kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
@@ -21,6 +20,7 @@ module.exports = {
kHost: Symbol('host'),
kNoRef: Symbol('no ref'),
kBodyUsed: Symbol('used'),
+ kBody: Symbol('abstracted request body'),
kRunning: Symbol('running'),
kBlocking: Symbol('blocking'),
kPending: Symbol('pending'),
@@ -33,6 +33,8 @@ module.exports = {
kNeedDrain: Symbol('need drain'),
kReset: Symbol('reset'),
kDestroyed: Symbol.for('nodejs.stream.destroyed'),
+ kResume: Symbol('resume'),
+ kOnError: Symbol('on error'),
kMaxHeadersSize: Symbol('max headers size'),
kRunningIdx: Symbol('running index'),
kPendingIdx: Symbol('pending index'),
@@ -54,10 +56,12 @@ module.exports = {
kMaxResponseSize: Symbol('max response size'),
kHTTP2Session: Symbol('http2Session'),
kHTTP2SessionState: Symbol('http2Session state'),
- kHTTP2BuildRequest: Symbol('http2 build request'),
- kHTTP1BuildRequest: Symbol('http1 build request'),
- kHTTP2CopyHeaders: Symbol('http2 copy headers'),
- kHTTPConnVersion: Symbol('http connection version'),
kRetryHandlerDefaultRetry: Symbol('retry agent default retry'),
- kConstruct: Symbol('constructable')
+ kConstruct: Symbol('constructable'),
+ kListeners: Symbol('listeners'),
+ kHTTPContext: Symbol('http context'),
+ kMaxConcurrentStreams: Symbol('max concurrent streams'),
+ kNoProxyAgent: Symbol('no proxy agent'),
+ kHttpProxyAgent: Symbol('http proxy agent'),
+ kHttpsProxyAgent: Symbol('https proxy agent')
}
diff --git a/node_modules/undici/lib/core/tree.js b/node_modules/undici/lib/core/tree.js
new file mode 100644
index 0000000..17dfca4
--- /dev/null
+++ b/node_modules/undici/lib/core/tree.js
@@ -0,0 +1,152 @@
+'use strict'
+
+const {
+ wellknownHeaderNames,
+ headerNameLowerCasedRecord
+} = require('./constants')
+
+class TstNode {
+ /** @type {any} */
+ value = null
+ /** @type {null | TstNode} */
+ left = null
+ /** @type {null | TstNode} */
+ middle = null
+ /** @type {null | TstNode} */
+ right = null
+ /** @type {number} */
+ code
+ /**
+ * @param {string} key
+ * @param {any} value
+ * @param {number} index
+ */
+ constructor (key, value, index) {
+ if (index === undefined || index >= key.length) {
+ throw new TypeError('Unreachable')
+ }
+ const code = this.code = key.charCodeAt(index)
+ // check code is ascii string
+ if (code > 0x7F) {
+ throw new TypeError('key must be ascii string')
+ }
+ if (key.length !== ++index) {
+ this.middle = new TstNode(key, value, index)
+ } else {
+ this.value = value
+ }
+ }
+
+ /**
+ * @param {string} key
+ * @param {any} value
+ */
+ add (key, value) {
+ const length = key.length
+ if (length === 0) {
+ throw new TypeError('Unreachable')
+ }
+ let index = 0
+ let node = this
+ while (true) {
+ const code = key.charCodeAt(index)
+ // check code is ascii string
+ if (code > 0x7F) {
+ throw new TypeError('key must be ascii string')
+ }
+ if (node.code === code) {
+ if (length === ++index) {
+ node.value = value
+ break
+ } else if (node.middle !== null) {
+ node = node.middle
+ } else {
+ node.middle = new TstNode(key, value, index)
+ break
+ }
+ } else if (node.code < code) {
+ if (node.left !== null) {
+ node = node.left
+ } else {
+ node.left = new TstNode(key, value, index)
+ break
+ }
+ } else if (node.right !== null) {
+ node = node.right
+ } else {
+ node.right = new TstNode(key, value, index)
+ break
+ }
+ }
+ }
+
+ /**
+ * @param {Uint8Array} key
+ * @return {TstNode | null}
+ */
+ search (key) {
+ const keylength = key.length
+ let index = 0
+ let node = this
+ while (node !== null && index < keylength) {
+ let code = key[index]
+ // A-Z
+ // First check if it is bigger than 0x5a.
+ // Lowercase letters have higher char codes than uppercase ones.
+ // Also we assume that headers will mostly contain lowercase characters.
+ if (code <= 0x5a && code >= 0x41) {
+ // Lowercase for uppercase.
+ code |= 32
+ }
+ while (node !== null) {
+ if (code === node.code) {
+ if (keylength === ++index) {
+ // Returns Node since it is the last key.
+ return node
+ }
+ node = node.middle
+ break
+ }
+ node = node.code < code ? node.left : node.right
+ }
+ }
+ return null
+ }
+}
+
+class TernarySearchTree {
+ /** @type {TstNode | null} */
+ node = null
+
+ /**
+ * @param {string} key
+ * @param {any} value
+ * */
+ insert (key, value) {
+ if (this.node === null) {
+ this.node = new TstNode(key, value, 0)
+ } else {
+ this.node.add(key, value)
+ }
+ }
+
+ /**
+ * @param {Uint8Array} key
+ * @return {any}
+ */
+ lookup (key) {
+ return this.node?.search(key)?.value ?? null
+ }
+}
+
+const tree = new TernarySearchTree()
+
+for (let i = 0; i < wellknownHeaderNames.length; ++i) {
+ const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]]
+ tree.insert(key, key)
+}
+
+module.exports = {
+ TernarySearchTree,
+ tree
+}
diff --git a/node_modules/undici/lib/core/util.js b/node_modules/undici/lib/core/util.js
index 5741650..9ee7ec2 100644
--- a/node_modules/undici/lib/core/util.js
+++ b/node_modules/undici/lib/core/util.js
@@ -1,18 +1,72 @@
'use strict'
-const assert = require('assert')
-const { kDestroyed, kBodyUsed } = require('./symbols')
-const { IncomingMessage } = require('http')
-const stream = require('stream')
-const net = require('net')
+const assert = require('node:assert')
+const { kDestroyed, kBodyUsed, kListeners, kBody } = require('./symbols')
+const { IncomingMessage } = require('node:http')
+const stream = require('node:stream')
+const net = require('node:net')
+const { Blob } = require('node:buffer')
+const nodeUtil = require('node:util')
+const { stringify } = require('node:querystring')
+const { EventEmitter: EE } = require('node:events')
const { InvalidArgumentError } = require('./errors')
-const { Blob } = require('buffer')
-const nodeUtil = require('util')
-const { stringify } = require('querystring')
const { headerNameLowerCasedRecord } = require('./constants')
+const { tree } = require('./tree')
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
+class BodyAsyncIterable {
+ constructor (body) {
+ this[kBody] = body
+ this[kBodyUsed] = false
+ }
+
+ async * [Symbol.asyncIterator] () {
+ assert(!this[kBodyUsed], 'disturbed')
+ this[kBodyUsed] = true
+ yield * this[kBody]
+ }
+}
+
+function wrapRequestBody (body) {
+ if (isStream(body)) {
+ // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
+ // so that it can be dispatched again?
+ // TODO (fix): Do we need 100-expect support to provide a way to do this properly?
+ if (bodyLength(body) === 0) {
+ body
+ .on('data', function () {
+ assert(false)
+ })
+ }
+
+ if (typeof body.readableDidRead !== 'boolean') {
+ body[kBodyUsed] = false
+ EE.prototype.on.call(body, 'data', function () {
+ this[kBodyUsed] = true
+ })
+ }
+
+ return body
+ } else if (body && typeof body.pipeTo === 'function') {
+ // TODO (fix): We can't access ReadableStream internal state
+ // to determine whether or not it has been disturbed. This is just
+ // a workaround.
+ return new BodyAsyncIterable(body)
+ } else if (
+ body &&
+ typeof body !== 'string' &&
+ !ArrayBuffer.isView(body) &&
+ isIterable(body)
+ ) {
+ // TODO: Should we allow re-using iterable if !this.opts.idempotent
+ // or through some other flag?
+ return new BodyAsyncIterable(body)
+ } else {
+ return body
+ }
+}
+
function nop () {}
function isStream (obj) {
@@ -21,13 +75,20 @@ function isStream (obj) {
// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
function isBlobLike (object) {
- return (Blob && object instanceof Blob) || (
- object &&
- typeof object === 'object' &&
- (typeof object.stream === 'function' ||
- typeof object.arrayBuffer === 'function') &&
- /^(Blob|File)$/.test(object[Symbol.toStringTag])
- )
+ if (object === null) {
+ return false
+ } else if (object instanceof Blob) {
+ return true
+ } else if (typeof object !== 'object') {
+ return false
+ } else {
+ const sTag = object[Symbol.toStringTag]
+
+ return (sTag === 'Blob' || sTag === 'File') && (
+ ('stream' in object && typeof object.stream === 'function') ||
+ ('arrayBuffer' in object && typeof object.arrayBuffer === 'function')
+ )
+ }
}
function buildURL (url, queryParams) {
@@ -44,11 +105,37 @@ function buildURL (url, queryParams) {
return url
}
+function isValidPort (port) {
+ const value = parseInt(port, 10)
+ return (
+ value === Number(port) &&
+ value >= 0 &&
+ value <= 65535
+ )
+}
+
+function isHttpOrHttpsPrefixed (value) {
+ return (
+ value != null &&
+ value[0] === 'h' &&
+ value[1] === 't' &&
+ value[2] === 't' &&
+ value[3] === 'p' &&
+ (
+ value[4] === ':' ||
+ (
+ value[4] === 's' &&
+ value[5] === ':'
+ )
+ )
+ )
+}
+
function parseURL (url) {
if (typeof url === 'string') {
url = new URL(url)
- if (!/^https?:/.test(url.origin || url.protocol)) {
+ if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
@@ -59,12 +146,8 @@ function parseURL (url) {
throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
}
- if (!/^https?:/.test(url.origin || url.protocol)) {
- throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
- }
-
if (!(url instanceof URL)) {
- if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
+ if (url.port != null && url.port !== '' && isValidPort(url.port) === false) {
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
}
@@ -84,28 +167,36 @@ function parseURL (url) {
throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
}
+ if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
+ }
+
const port = url.port != null
? url.port
: (url.protocol === 'https:' ? 443 : 80)
let origin = url.origin != null
? url.origin
- : `${url.protocol}//${url.hostname}:${port}`
+ : `${url.protocol || ''}//${url.hostname || ''}:${port}`
let path = url.path != null
? url.path
: `${url.pathname || ''}${url.search || ''}`
- if (origin.endsWith('/')) {
- origin = origin.substring(0, origin.length - 1)
+ if (origin[origin.length - 1] === '/') {
+ origin = origin.slice(0, origin.length - 1)
}
- if (path && !path.startsWith('/')) {
+ if (path && path[0] !== '/') {
path = `/${path}`
}
// new URL(path, origin) is unsafe when `path` contains an absolute URL
// From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
// If first parameter is a relative URL, second param is required, and will be used as the base URL.
// If first parameter is an absolute URL, a given second param will be ignored.
- url = new URL(origin + path)
+ return new URL(`${origin}${path}`)
+ }
+
+ if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
+ throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
}
return url
@@ -142,7 +233,7 @@ function getServerName (host) {
return null
}
- assert.strictEqual(typeof host, 'string')
+ assert(typeof host === 'string')
const servername = getHostname(host)
if (net.isIP(servername)) {
@@ -181,13 +272,8 @@ function bodyLength (body) {
return null
}
-function isDestroyed (stream) {
- return !stream || !!(stream.destroyed || stream[kDestroyed])
-}
-
-function isReadableAborted (stream) {
- const state = stream && stream._readableState
- return isDestroyed(stream) && state && !state.endEmitted
+function isDestroyed (body) {
+ return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body)))
}
function destroy (stream, err) {
@@ -203,9 +289,9 @@ function destroy (stream, err) {
stream.destroy(err)
} else if (err) {
- process.nextTick((stream, err) => {
+ queueMicrotask(() => {
stream.emit('error', err)
- }, stream, err)
+ })
}
if (stream.destroyed !== true) {
@@ -225,29 +311,44 @@ function parseKeepAliveTimeout (val) {
* @returns {string}
*/
function headerNameToString (value) {
- return headerNameLowerCasedRecord[value] || value.toLowerCase()
+ return typeof value === 'string'
+ ? headerNameLowerCasedRecord[value] ?? value.toLowerCase()
+ : tree.lookup(value) ?? value.toString('latin1').toLowerCase()
}
-function parseHeaders (headers, obj = {}) {
- // For H2 support
- if (!Array.isArray(headers)) return headers
+/**
+ * Receive the buffer as a string and return its lowercase value.
+ * @param {Buffer} value Header name
+ * @returns {string}
+ */
+function bufferToLowerCasedHeaderName (value) {
+ return tree.lookup(value) ?? value.toString('latin1').toLowerCase()
+}
+/**
+ * @param {Record<string, string | string[]> | (Buffer | string | (Buffer | string)[])[]} headers
+ * @param {Record<string, string | string[]>} [obj]
+ * @returns {Record<string, string | string[]>}
+ */
+function parseHeaders (headers, obj) {
+ if (obj === undefined) obj = {}
for (let i = 0; i < headers.length; i += 2) {
- const key = headers[i].toString().toLowerCase()
+ const key = headerNameToString(headers[i])
let val = obj[key]
- if (!val) {
- if (Array.isArray(headers[i + 1])) {
- obj[key] = headers[i + 1].map(x => x.toString('utf8'))
- } else {
- obj[key] = headers[i + 1].toString('utf8')
- }
- } else {
- if (!Array.isArray(val)) {
+ if (val) {
+ if (typeof val === 'string') {
val = [val]
obj[key] = val
}
val.push(headers[i + 1].toString('utf8'))
+ } else {
+ const headersValue = headers[i + 1]
+ if (typeof headersValue === 'string') {
+ obj[key] = headersValue
+ } else {
+ obj[key] = Array.isArray(headersValue) ? headersValue.map(x => x.toString('utf8')) : headersValue.toString('utf8')
+ }
}
}
@@ -260,22 +361,30 @@ function parseHeaders (headers, obj = {}) {
}
function parseRawHeaders (headers) {
- const ret = []
+ const len = headers.length
+ const ret = new Array(len)
+
let hasContentLength = false
let contentDispositionIdx = -1
+ let key
+ let val
+ let kLen = 0
for (let n = 0; n < headers.length; n += 2) {
- const key = headers[n + 0].toString()
- const val = headers[n + 1].toString('utf8')
+ key = headers[n]
+ val = headers[n + 1]
+
+ typeof key !== 'string' && (key = key.toString())
+ typeof val !== 'string' && (val = val.toString('utf8'))
- if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
- ret.push(key, val)
+ kLen = key.length
+ if (kLen === 14 && key[7] === '-' && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
hasContentLength = true
- } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
- contentDispositionIdx = ret.push(key, val) - 1
- } else {
- ret.push(key, val)
+ } else if (kLen === 19 && key[7] === '-' && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
+ contentDispositionIdx = n + 1
}
+ ret[n] = key
+ ret[n + 1] = val
}
// See https://github.com/nodejs/node/pull/46528
@@ -330,30 +439,16 @@ function validateHandler (handler, method, upgrade) {
// A body is disturbed if it has been read from and it cannot
// be re-used without losing state or data.
function isDisturbed (body) {
- return !!(body && (
- stream.isDisturbed
- ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
- : body[kBodyUsed] ||
- body.readableDidRead ||
- (body._readableState && body._readableState.dataEmitted) ||
- isReadableAborted(body)
- ))
+ // TODO (fix): Why is body[kBodyUsed] needed?
+ return !!(body && (stream.isDisturbed(body) || body[kBodyUsed]))
}
function isErrored (body) {
- return !!(body && (
- stream.isErrored
- ? stream.isErrored(body)
- : /state: 'errored'/.test(nodeUtil.inspect(body)
- )))
+ return !!(body && stream.isErrored(body))
}
function isReadable (body) {
- return !!(body && (
- stream.isReadable
- ? stream.isReadable(body)
- : /state: 'readable'/.test(nodeUtil.inspect(body)
- )))
+ return !!(body && stream.isReadable(body))
}
function getSocketInfo (socket) {
@@ -369,21 +464,9 @@ function getSocketInfo (socket) {
}
}
-async function * convertIterableToBuffer (iterable) {
- for await (const chunk of iterable) {
- yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
- }
-}
-
-let ReadableStream
+/** @type {globalThis['ReadableStream']} */
function ReadableStreamFrom (iterable) {
- if (!ReadableStream) {
- ReadableStream = require('stream/web').ReadableStream
- }
-
- if (ReadableStream.from) {
- return ReadableStream.from(convertIterableToBuffer(iterable))
- }
+ // We cannot use ReadableStream.from here because it does not return a byte stream.
let iterator
return new ReadableStream(
@@ -396,18 +479,21 @@ function ReadableStreamFrom (iterable) {
if (done) {
queueMicrotask(() => {
controller.close()
+ controller.byobRequest?.respond(0)
})
} else {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
- controller.enqueue(new Uint8Array(buf))
+ if (buf.byteLength) {
+ controller.enqueue(new Uint8Array(buf))
+ }
}
return controller.desiredSize > 0
},
async cancel (reason) {
await iterator.return()
- }
- },
- 0
+ },
+ type: 'bytes'
+ }
)
}
@@ -427,20 +513,6 @@ function isFormDataLike (object) {
)
}
-function throwIfAborted (signal) {
- if (!signal) { return }
- if (typeof signal.throwIfAborted === 'function') {
- signal.throwIfAborted()
- } else {
- if (signal.aborted) {
- // DOMException not available < v17.0.0
- const err = new Error('The operation was aborted')
- err.name = 'AbortError'
- throw err
- }
- }
-}
-
function addAbortListener (signal, listener) {
if ('addEventListener' in signal) {
signal.addEventListener('abort', listener, { once: true })
@@ -450,19 +522,86 @@ function addAbortListener (signal, listener) {
return () => signal.removeListener('abort', listener)
}
-const hasToWellFormed = !!String.prototype.toWellFormed
+const hasToWellFormed = typeof String.prototype.toWellFormed === 'function'
+const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function'
/**
* @param {string} val
*/
function toUSVString (val) {
- if (hasToWellFormed) {
- return `${val}`.toWellFormed()
- } else if (nodeUtil.toUSVString) {
- return nodeUtil.toUSVString(val)
+ return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
+}
+
+/**
+ * @param {string} val
+ */
+// TODO: move this to webidl
+function isUSVString (val) {
+ return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`
+}
+
+/**
+ * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
+ * @param {number} c
+ */
+function isTokenCharCode (c) {
+ switch (c) {
+ case 0x22:
+ case 0x28:
+ case 0x29:
+ case 0x2c:
+ case 0x2f:
+ case 0x3a:
+ case 0x3b:
+ case 0x3c:
+ case 0x3d:
+ case 0x3e:
+ case 0x3f:
+ case 0x40:
+ case 0x5b:
+ case 0x5c:
+ case 0x5d:
+ case 0x7b:
+ case 0x7d:
+ // DQUOTE and "(),/:;<=>?@[\]{}"
+ return false
+ default:
+ // VCHAR %x21-7E
+ return c >= 0x21 && c <= 0x7e
+ }
+}
+
+/**
+ * @param {string} characters
+ */
+function isValidHTTPToken (characters) {
+ if (characters.length === 0) {
+ return false
}
+ for (let i = 0; i < characters.length; ++i) {
+ if (!isTokenCharCode(characters.charCodeAt(i))) {
+ return false
+ }
+ }
+ return true
+}
+
+// headerCharRegex have been lifted from
+// https://github.com/nodejs/node/blob/main/lib/_http_common.js
- return `${val}`
+/**
+ * Matches if val contains an invalid field-vchar
+ * field-value = *( field-content / obs-fold )
+ * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+ * field-vchar = VCHAR / obs-text
+ */
+const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
+
+/**
+ * @param {string} characters
+ */
+function isValidHeaderValue (characters) {
+ return !headerCharRegex.test(characters)
}
// Parsed accordingly to RFC 9110
@@ -480,9 +619,57 @@ function parseRangeHeader (range) {
: null
}
+function addListener (obj, name, listener) {
+ const listeners = (obj[kListeners] ??= [])
+ listeners.push([name, listener])
+ obj.on(name, listener)
+ return obj
+}
+
+function removeAllListeners (obj) {
+ for (const [name, listener] of obj[kListeners] ?? []) {
+ obj.removeListener(name, listener)
+ }
+ obj[kListeners] = null
+}
+
+function errorRequest (client, request, err) {
+ try {
+ request.onError(err)
+ assert(request.aborted)
+ } catch (err) {
+ client.emit('error', err)
+ }
+}
+
const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true
+const normalizedMethodRecordsBase = {
+ delete: 'DELETE',
+ DELETE: 'DELETE',
+ get: 'GET',
+ GET: 'GET',
+ head: 'HEAD',
+ HEAD: 'HEAD',
+ options: 'OPTIONS',
+ OPTIONS: 'OPTIONS',
+ post: 'POST',
+ POST: 'POST',
+ put: 'PUT',
+ PUT: 'PUT'
+}
+
+const normalizedMethodRecords = {
+ ...normalizedMethodRecordsBase,
+ patch: 'patch',
+ PATCH: 'PATCH'
+}
+
+// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
+Object.setPrototypeOf(normalizedMethodRecordsBase, null)
+Object.setPrototypeOf(normalizedMethodRecords, null)
+
module.exports = {
kEnumerableProperty,
nop,
@@ -490,7 +677,7 @@ module.exports = {
isErrored,
isReadable,
toUSVString,
- isReadableAborted,
+ isUSVString,
isBlobLike,
parseOrigin,
parseURL,
@@ -500,6 +687,10 @@ module.exports = {
isAsyncIterable,
isDestroyed,
headerNameToString,
+ bufferToLowerCasedHeaderName,
+ addListener,
+ removeAllListeners,
+ errorRequest,
parseRawHeaders,
parseHeaders,
parseKeepAliveTimeout,
@@ -512,11 +703,17 @@ module.exports = {
getSocketInfo,
isFormDataLike,
buildURL,
- throwIfAborted,
addAbortListener,
+ isValidHTTPToken,
+ isValidHeaderValue,
+ isTokenCharCode,
parseRangeHeader,
+ normalizedMethodRecordsBase,
+ normalizedMethodRecords,
+ isValidPort,
+ isHttpOrHttpsPrefixed,
nodeMajor,
nodeMinor,
- nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
- safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
+ safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
+ wrapRequestBody
}