aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/node_modules/undici/lib/core/util.js
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/util.js
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/util.js')
-rw-r--r--node_modules/undici/lib/core/util.js433
1 files changed, 315 insertions, 118 deletions
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
}