deps: update undici to 6.20.0

PR-URL: https://github.com/nodejs/node/pull/55329
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Reviewed-By: Matthew Aitken <maitken033380023@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Node.js GitHub Bot authored and Antoine du Hamel committed 2024-10-11 09:04:15 +02:00
1 parent ff3cc3b2ab
commit 34ca36a397
27 files changed
+1532 -612

No files matched your search

+3
View File
@@ -84,3 +84,6 @@ undici-fetch.js
.npmrc
.tap
# File generated by /test/request-timeout.js
test/request-timeout.10mb.bin
+3
View File
@@ -11,3 +11,6 @@ lib/llhttp/llhttp.wasm
!index.d.ts
!docs/docs/**/*
!scripts/strip-comments.js
# File generated by /test/request-timeout.js
test/request-timeout.10mb.bin
+197
View File
@@ -984,6 +984,203 @@ client.dispatch(
);
```
##### `Response Error Interceptor`
**Introduction**
The Response Error Interceptor is designed to handle HTTP response errors efficiently. It intercepts responses and throws detailed errors for responses with status codes indicating failure (4xx, 5xx). This interceptor enhances error handling by providing structured error information, including response headers, data, and status codes.
**ResponseError Class**
The `ResponseError` class extends the `UndiciError` class and encapsulates detailed error information. It captures the response status code, headers, and data, providing a structured way to handle errors.
**Definition**
```js
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;
}
}
```
**Interceptor Handler**
The interceptor's handler class extends `DecoratorHandler` and overrides methods to capture response details and handle errors based on the response status code.
**Methods**
- **onConnect**: Initializes response properties.
- **onHeaders**: Captures headers and status code. Decodes body if content type is `application/json` or `text/plain`.
- **onData**: Appends chunks to the body if status code indicates an error.
- **onComplete**: Finalizes error handling, constructs a `ResponseError`, and invokes the `onError` method.
- **onError**: Propagates errors to the handler.
**Definition**
```js
class Handler extends DecoratorHandler {
// Private properties
#handler;
#statusCode;
#contentType;
#decoder;
#headers;
#body;
constructor (opts, { handler }) {
super(handler);
this.#handler = handler;
}
onConnect (abort) {
this.#statusCode = 0;
this.#contentType = null;
this.#decoder = null;
this.#headers = null;
this.#body = '';
return this.#handler.onConnect(abort);
}
onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
this.#statusCode = statusCode;
this.#headers = headers;
this.#contentType = headers['content-type'];
if (this.#statusCode < 400) {
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers);
}
if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
this.#decoder = new TextDecoder('utf-8');
}
}
onData (chunk) {
if (this.#statusCode < 400) {
return this.#handler.onData(chunk);
}
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? '';
}
onComplete (rawTrailers) {
if (this.#statusCode >= 400) {
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? '';
if (this.#contentType === 'application/json') {
try {
this.#body = JSON.parse(this.#body);
} catch {
// Do nothing...
}
}
let err;
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
try {
err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body);
} finally {
Error.stackTraceLimit = stackTraceLimit;
}
this.#handler.onError(err);
} else {
this.#handler.onComplete(rawTrailers);
}
}
onError (err) {
this.#handler.onError(err);
}
}
module.exports = (dispatch) => (opts, handler) => opts.throwOnError
? dispatch(opts, new Handler(opts, { handler }))
: dispatch(opts, handler);
```
**Tests**
Unit tests ensure the interceptor functions correctly, handling both error and non-error responses appropriately.
**Example Tests**
- **No Error if `throwOnError` is False**:
```js
test('should not error if request is not meant to throw error', async (t) => {
const opts = { throwOnError: false };
const handler = { onError: () => {}, onData: () => {}, onComplete: () => {} };
const interceptor = createResponseErrorInterceptor((opts, handler) => handler.onComplete());
assert.doesNotThrow(() => interceptor(opts, handler));
});
```
- **Error if Status Code is in Specified Error Codes**:
```js
test('should error if request status code is in the specified error codes', async (t) => {
const opts = { throwOnError: true, statusCodes: [500] };
const response = { statusCode: 500 };
let capturedError;
const handler = {
onError: (err) => { capturedError = err; },
onData: () => {},
onComplete: () => {}
};
const interceptor = createResponseErrorInterceptor((opts, handler) => {
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
handler.onError(new Error('Response Error'));
} else {
handler.onComplete();
}
});
interceptor({ ...opts, response }, handler);
await new Promise(resolve => setImmediate(resolve));
assert(capturedError, 'Expected error to be captured but it was not.');
assert.strictEqual(capturedError.message, 'Response Error');
assert.strictEqual(response.statusCode, 500);
});
```
- **No Error if Status Code is Not in Specified Error Codes**:
```js
test('should not error if request status code is not in the specified error codes', async (t) => {
const opts = { throwOnError: true, statusCodes: [500] };
const response = { statusCode: 404 };
const handler = {
onError: () => {},
onData: () => {},
onComplete: () => {}
};
const interceptor = createResponseErrorInterceptor((opts, handler) => {
if (opts.throwOnError && opts.statusCodes.includes(response.statusCode)) {
handler.onError(new Error('Response Error'));
} else {
handler.onComplete();
}
});
assert.doesNotThrow(() => interceptor({ ...opts, response }, handler));
});
```
**Conclusion**
The Response Error Interceptor provides a robust mechanism for handling HTTP response errors by capturing detailed error information and propagating it through a structured `ResponseError` class. This enhancement improves error handling and debugging capabilities in applications using the interceptor.
## Instance Events
### Event: `'connect'`
+1 -1
View File
@@ -19,7 +19,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
#### `RetryOptions`
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => number | null` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
+2 -2
View File
@@ -50,9 +50,9 @@ class UpgradeHandler extends AsyncResource {
}
onUpgrade (statusCode, rawHeaders, socket) {
const { callback, opaque, context } = this
assert(statusCode === 101)
assert.strictEqual(statusCode, 101)
const { callback, opaque, context } = this
removeSignal(this)
+73 -31
View File
@@ -4,6 +4,9 @@ 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
@@ -91,9 +94,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
servername = servername || options.servername || util.getServerName(host) || null
const sessionKey = servername || hostname
assert(sessionKey)
const session = customSession || sessionCache.get(sessionKey) || null
assert(sessionKey)
port = port || 443
socket = tls.connect({
highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
@@ -104,7 +109,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
// 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, sess
})
} 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, sess
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, sess
}
})
.on('error', function (err) {
cancelTimeout()
queueMicrotask(clearConnectTimeout)
if (callback) {
const cb = callback
@@ -157,36 +165,70 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess
}
}
function setupTimeout (onConnectTimeout, timeout) {
if (!timeout) {
return () => {}
}
let s1 = null
let s2 = null
const timeoutId = setTimeout(() => {
// setImmediate is added to make sure that we prioritize socket error events over timeouts
s1 = setImmediate(() => {
if (process.platform === 'win32') {
// Windows needs an extra setImmediate probably due to implementation differences in the socket logic
s2 = setImmediate(() => onConnectTimeout())
} else {
onConnectTimeout()
/**
* @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
}
})
}, timeout)
return () => {
clearTimeout(timeoutId)
clearImmediate(s1)
clearImmediate(s2)
}
}
function onConnectTimeout (socket) {
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(socketWeakRef.deref(), opts))
})
}, opts.timeout)
return () => {
timers.clearFastTimeout(fastTimer)
clearImmediate(s1)
clearImmediate(s2)
}
}
: (socketWeakRef, opts) => {
if (!opts.timeout) {
return noop
}
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) {
let message = 'Connect Timeout Error'
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')})`
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))
}
+13
View File
@@ -195,6 +195,18 @@ class RequestRetryError extends UndiciError {
}
}
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
}
}
class SecureProxyConnectionError extends UndiciError {
constructor (cause, message, options) {
super(message, { cause, ...(options ?? {}) })
@@ -227,5 +239,6 @@ module.exports = {
BalancedPoolMissingUpstreamError,
ResponseExceededMaxSizeError,
RequestRetryError,
ResponseError,
SecureProxyConnectionError
}
+1 -1
View File
@@ -233,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)) {
+63 -43
View File
@@ -85,35 +85,35 @@ async function lazyllhttp () {
return 0
},
wasm_on_status: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
},
wasm_on_message_begin: (p) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
return currentParser.onMessageBegin() || 0
},
wasm_on_header_field: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
},
wasm_on_header_value: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
},
wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0
},
wasm_on_body: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
const start = at - currentBufferPtr + currentBufferRef.byteOffset
return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0
},
wasm_on_message_complete: (p) => {
assert.strictEqual(currentParser.ptr, p)
assert(currentParser.ptr === p)
return currentParser.onMessageComplete() || 0
}
@@ -131,9 +131,17 @@ let currentBufferRef = null
let currentBufferSize = 0
let currentBufferPtr = null
const TIMEOUT_HEADERS = 1
const TIMEOUT_BODY = 2
const TIMEOUT_IDLE = 3
const USE_NATIVE_TIMER = 0
const USE_FAST_TIMER = 1
// Use fast timers for headers and body to take eventual event loop
// latency into account.
const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER
const TIMEOUT_BODY = 4 | USE_FAST_TIMER
// Use native timers to ignore event loop latency for keep-alive
// handling.
const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER
class Parser {
constructor (client, socket, { exports }) {
@@ -164,26 +172,39 @@ class Parser {
this.maxResponseSize = client[kMaxResponseSize]
}
setTimeout (value, type) {
this.timeoutType = type
if (value !== this.timeoutValue) {
timers.clearTimeout(this.timeout)
if (value) {
this.timeout = timers.setTimeout(onParserTimeout, value, this)
// istanbul ignore else: only for jest
if (this.timeout.unref) {
this.timeout.unref()
}
} else {
setTimeout (delay, type) {
// If the existing timer and the new timer are of different timer type
// (fast or native) or have different delay, we need to clear the existing
// timer and set a new one.
if (
delay !== this.timeoutValue ||
(type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER)
) {
// If a timeout is already set, clear it with clearTimeout of the fast
// timer implementation, as it can clear fast and native timers.
if (this.timeout) {
timers.clearTimeout(this.timeout)
this.timeout = null
}
this.timeoutValue = value
if (delay) {
if (type & USE_FAST_TIMER) {
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
} else {
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
this.timeout.unref()
}
}
this.timeoutValue = delay
} else if (this.timeout) {
// istanbul ignore else: only for jest
if (this.timeout.refresh) {
this.timeout.refresh()
}
}
this.timeoutType = type
}
resume () {
@@ -288,7 +309,7 @@ class Parser {
this.llhttp.llhttp_free(this.ptr)
this.ptr = null
timers.clearTimeout(this.timeout)
this.timeout && timers.clearTimeout(this.timeout)
this.timeout = null
this.timeoutValue = null
this.timeoutType = null
@@ -363,20 +384,19 @@ class Parser {
const { upgrade, client, socket, headers, statusCode } = this
assert(upgrade)
assert(client[kSocket] === socket)
assert(!socket.destroyed)
assert(!this.paused)
assert((headers.length & 1) === 0)
const request = client[kQueue][client[kRunningIdx]]
assert(request)
assert(!socket.destroyed)
assert(socket === client[kSocket])
assert(!this.paused)
assert(request.upgrade || request.method === 'CONNECT')
this.statusCode = null
this.statusText = ''
this.shouldKeepAlive = null
assert(this.headers.length % 2 === 0)
this.headers = []
this.headersSize = 0
@@ -433,7 +453,7 @@ class Parser {
return -1
}
assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS)
assert(this.timeoutType === TIMEOUT_HEADERS)
this.statusCode = statusCode
this.shouldKeepAlive = (
@@ -466,7 +486,7 @@ class Parser {
return 2
}
assert(this.headers.length % 2 === 0)
assert((this.headers.length & 1) === 0)
this.headers = []
this.headersSize = 0
@@ -523,7 +543,7 @@ class Parser {
const request = client[kQueue][client[kRunningIdx]]
assert(request)
assert.strictEqual(this.timeoutType, TIMEOUT_BODY)
assert(this.timeoutType === TIMEOUT_BODY)
if (this.timeout) {
// istanbul ignore else: only for jest
if (this.timeout.refresh) {
@@ -556,11 +576,12 @@ class Parser {
return
}
assert(statusCode >= 100)
assert((this.headers.length & 1) === 0)
const request = client[kQueue][client[kRunningIdx]]
assert(request)
assert(statusCode >= 100)
this.statusCode = null
this.statusText = ''
this.bytesRead = 0
@@ -568,7 +589,6 @@ class Parser {
this.keepAlive = ''
this.connection = ''
assert(this.headers.length % 2 === 0)
this.headers = []
this.headersSize = 0
@@ -587,7 +607,7 @@ class Parser {
client[kQueue][client[kRunningIdx]++] = null
if (socket[kWriting]) {
assert.strictEqual(client[kRunning], 0)
assert(client[kRunning] === 0)
// Response completed before request.
util.destroy(socket, new InformationalError('reset'))
return constants.ERROR.PAUSED
@@ -613,19 +633,19 @@ class Parser {
}
function onParserTimeout (parser) {
const { socket, timeoutType, client } = parser
const { socket, timeoutType, client, paused } = parser.deref()
/* istanbul ignore else */
if (timeoutType === TIMEOUT_HEADERS) {
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
assert(!parser.paused, 'cannot be paused while waiting for headers')
assert(!paused, 'cannot be paused while waiting for headers')
util.destroy(socket, new HeadersTimeoutError())
}
} else if (timeoutType === TIMEOUT_BODY) {
if (!parser.paused) {
if (!paused) {
util.destroy(socket, new BodyTimeoutError())
}
} else if (timeoutType === TIMEOUT_IDLE) {
} else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue])
util.destroy(socket, new InformationalError('socket idle timeout'))
}
@@ -646,10 +666,10 @@ async function connectH1 (client, socket) {
socket[kParser] = new Parser(client, socket, llhttpInstance)
addListener(socket, 'error', function (err) {
const parser = this[kParser]
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
const parser = this[kParser]
// On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
// to the user.
if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
@@ -803,8 +823,8 @@ function resumeH1 (client) {
}
if (client[kSize] === 0) {
if (socket[kParser].timeoutType !== TIMEOUT_IDLE) {
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE)
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE)
}
} else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
+4
View File
@@ -385,6 +385,10 @@ function onError (client, err) {
}
}
/**
* @param {Client} client
* @returns
*/
async function connect (client) {
assert(!client[kConnecting])
assert(!client[kHTTPContext])
+12 -2
View File
@@ -192,8 +192,18 @@ class RetryHandler {
if (this.resume != null) {
this.resume = null
if (statusCode !== 206) {
return true
// Only Partial Content 206 supposed to provide Content-Range,
// any other status code that partially consumed the payload
// should not be retry because it would result in downstream
// wrongly concatanete multiple responses.
if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
this.abort(
new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
headers,
data: { count: this.retryCount }
})
)
return false
}
const contentRange = parseRangeHeader(headers['content-range'])
+86
View File
@@ -0,0 +1,86 @@
'use strict'
const { parseHeaders } = require('../core/util')
const DecoratorHandler = require('../handler/decorator-handler')
const { ResponseError } = require('../core/errors')
class Handler extends DecoratorHandler {
#handler
#statusCode
#contentType
#decoder
#headers
#body
constructor (opts, { handler }) {
super(handler)
this.#handler = handler
}
onConnect (abort) {
this.#statusCode = 0
this.#contentType = null
this.#decoder = null
this.#headers = null
this.#body = ''
return this.#handler.onConnect(abort)
}
onHeaders (statusCode, rawHeaders, resume, statusMessage, headers = parseHeaders(rawHeaders)) {
this.#statusCode = statusCode
this.#headers = headers
this.#contentType = headers['content-type']
if (this.#statusCode < 400) {
return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
}
if (this.#contentType === 'application/json' || this.#contentType === 'text/plain') {
this.#decoder = new TextDecoder('utf-8')
}
}
onData (chunk) {
if (this.#statusCode < 400) {
return this.#handler.onData(chunk)
}
this.#body += this.#decoder?.decode(chunk, { stream: true }) ?? ''
}
onComplete (rawTrailers) {
if (this.#statusCode >= 400) {
this.#body += this.#decoder?.decode(undefined, { stream: false }) ?? ''
if (this.#contentType === 'application/json') {
try {
this.#body = JSON.parse(this.#body)
} catch {
// Do nothing...
}
}
let err
const stackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = 0
try {
err = new ResponseError('Response Error', this.#statusCode, this.#headers, this.#body)
} finally {
Error.stackTraceLimit = stackTraceLimit
}
this.#handler.onError(err)
} else {
this.#handler.onComplete(rawTrailers)
}
}
onError (err) {
this.#handler.onError(err)
}
}
module.exports = (dispatch) => (opts, handler) => opts.throwOnError
? dispatch(opts, new Handler(opts, { handler }))
: dispatch(opts, handler)
+2 -2
View File
@@ -1,12 +1,12 @@
> undici@6.19.8 prebuild:wasm
> undici@6.20.0 prebuild:wasm
> node build/wasm.js --prebuild
> docker build --platform=linux/x86_64 -t llhttp_wasm_builder -f /home/runner/work/node/node/deps/undici/src/build/Dockerfile /home/runner/work/node/node/deps/undici/src
> undici@6.19.8 build:wasm
> undici@6.20.0 build:wasm
> node build/wasm.js --docker
> docker run --rm -t --platform=linux/x86_64 --user 1001:127 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/undici/lib/llhttp llhttp_wasm_builder node build/wasm.js
+4
View File
@@ -118,6 +118,10 @@ function matchKey (mockDispatch, { path, method, body, headers }) {
function getResponseData (data) {
if (Buffer.isBuffer(data)) {
return data
} else if (data instanceof Uint8Array) {
return data
} else if (data instanceof ArrayBuffer) {
return data
} else if (typeof data === 'object') {
return JSON.stringify(data)
} else {
+367 -43
View File
@@ -1,99 +1,423 @@
'use strict'
const TICK_MS = 499
/**
* This module offers an optimized timer implementation designed for scenarios
* where high precision is not critical.
*
* The timer achieves faster performance by using a low-resolution approach,
* with an accuracy target of within 500ms. This makes it particularly useful
* for timers with delays of 1 second or more, where exact timing is less
* crucial.
*
* It's important to note that Node.js timers are inherently imprecise, as
* delays can occur due to the event loop being blocked by other operations.
* Consequently, timers may trigger later than their scheduled time.
*/
let fastNow = Date.now()
/**
* The fastNow variable contains the internal fast timer clock value.
*
* @type {number}
*/
let fastNow = 0
/**
* RESOLUTION_MS represents the target resolution time in milliseconds.
*
* @type {number}
* @default 1000
*/
const RESOLUTION_MS = 1e3
/**
* TICK_MS defines the desired interval in milliseconds between each tick.
* The target value is set to half the resolution time, minus 1 ms, to account
* for potential event loop overhead.
*
* @type {number}
* @default 499
*/
const TICK_MS = (RESOLUTION_MS >> 1) - 1
/**
* fastNowTimeout is a Node.js timer used to manage and process
* the FastTimers stored in the `fastTimers` array.
*
* @type {NodeJS.Timeout}
*/
let fastNowTimeout
/**
* The kFastTimer symbol is used to identify FastTimer instances.
*
* @type {Symbol}
*/
const kFastTimer = Symbol('kFastTimer')
/**
* The fastTimers array contains all active FastTimers.
*
* @type {FastTimer[]}
*/
const fastTimers = []
function onTimeout () {
fastNow = Date.now()
/**
* These constants represent the various states of a FastTimer.
*/
let len = fastTimers.length
/**
* The `NOT_IN_LIST` constant indicates that the FastTimer is not included
* in the `fastTimers` array. Timers with this status will not be processed
* during the next tick by the `onTick` function.
*
* A FastTimer can be re-added to the `fastTimers` array by invoking the
* `refresh` method on the FastTimer instance.
*
* @type {-2}
*/
const NOT_IN_LIST = -2
/**
* The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled
* for removal from the `fastTimers` array. A FastTimer in this state will
* be removed in the next tick by the `onTick` function and will no longer
* be processed.
*
* This status is also set when the `clear` method is called on the FastTimer instance.
*
* @type {-1}
*/
const TO_BE_CLEARED = -1
/**
* The `PENDING` constant signifies that the FastTimer is awaiting processing
* in the next tick by the `onTick` function. Timers with this status will have
* their `_idleStart` value set and their status updated to `ACTIVE` in the next tick.
*
* @type {0}
*/
const PENDING = 0
/**
* The `ACTIVE` constant indicates that the FastTimer is active and waiting
* for its timer to expire. During the next tick, the `onTick` function will
* check if the timer has expired, and if so, it will execute the associated callback.
*
* @type {1}
*/
const ACTIVE = 1
/**
* The onTick function processes the fastTimers array.
*
* @returns {void}
*/
function onTick () {
/**
* Increment the fastNow value by the TICK_MS value, despite the actual time
* that has passed since the last tick. This approach ensures independence
* from the system clock and delays caused by a blocked event loop.
*
* @type {number}
*/
fastNow += TICK_MS
/**
* The `idx` variable is used to iterate over the `fastTimers` array.
* Expired timers are removed by replacing them with the last element in the array.
* Consequently, `idx` is only incremented when the current element is not removed.
*
* @type {number}
*/
let idx = 0
/**
* The len variable will contain the length of the fastTimers array
* and will be decremented when a FastTimer should be removed from the
* fastTimers array.
*
* @type {number}
*/
let len = fastTimers.length
while (idx < len) {
/**
* @type {FastTimer}
*/
const timer = fastTimers[idx]
if (timer.state === 0) {
timer.state = fastNow + timer.delay - TICK_MS
} else if (timer.state > 0 && fastNow >= timer.state) {
timer.state = -1
timer.callback(timer.opaque)
// If the timer is in the ACTIVE state and the timer has expired, it will
// be processed in the next tick.
if (timer._state === PENDING) {
// Set the _idleStart value to the fastNow value minus the TICK_MS value
// to account for the time the timer was in the PENDING state.
timer._idleStart = fastNow - TICK_MS
timer._state = ACTIVE
} else if (
timer._state === ACTIVE &&
fastNow >= timer._idleStart + timer._idleTimeout
) {
timer._state = TO_BE_CLEARED
timer._idleStart = -1
timer._onTimeout(timer._timerArg)
}
if (timer.state === -1) {
timer.state = -2
if (idx !== len - 1) {
fastTimers[idx] = fastTimers.pop()
} else {
fastTimers.pop()
if (timer._state === TO_BE_CLEARED) {
timer._state = NOT_IN_LIST
// Move the last element to the current index and decrement len if it is
// not the only element in the array.
if (--len !== 0) {
fastTimers[idx] = fastTimers[len]
}
len -= 1
} else {
idx += 1
++idx
}
}
if (fastTimers.length > 0) {
// Set the length of the fastTimers array to the new length and thus
// removing the excess FastTimers elements from the array.
fastTimers.length = len
// If there are still active FastTimers in the array, refresh the Timer.
// If there are no active FastTimers, the timer will be refreshed again
// when a new FastTimer is instantiated.
if (fastTimers.length !== 0) {
refreshTimeout()
}
}
function refreshTimeout () {
if (fastNowTimeout?.refresh) {
// If the fastNowTimeout is already set, refresh it.
if (fastNowTimeout) {
fastNowTimeout.refresh()
// fastNowTimeout is not instantiated yet, create a new Timer.
} else {
clearTimeout(fastNowTimeout)
fastNowTimeout = setTimeout(onTimeout, TICK_MS)
fastNowTimeout = setTimeout(onTick, TICK_MS)
// If the Timer has an unref method, call it to allow the process to exit if
// there are no other active handles.
if (fastNowTimeout.unref) {
fastNowTimeout.unref()
}
}
}
class Timeout {
constructor (callback, delay, opaque) {
this.callback = callback
this.delay = delay
this.opaque = opaque
/**
* The `FastTimer` class is a data structure designed to store and manage
* timer information.
*/
class FastTimer {
[kFastTimer] = true
// -2 not in timer list
// -1 in timer list but inactive
// 0 in timer list waiting for time
// > 0 in timer list waiting for time to expire
this.state = -2
/**
* The state of the timer, which can be one of the following:
* - NOT_IN_LIST (-2)
* - TO_BE_CLEARED (-1)
* - PENDING (0)
* - ACTIVE (1)
*
* @type {-2|-1|0|1}
* @private
*/
_state = NOT_IN_LIST
/**
* The number of milliseconds to wait before calling the callback.
*
* @type {number}
* @private
*/
_idleTimeout = -1
/**
* The time in milliseconds when the timer was started. This value is used to
* calculate when the timer should expire.
*
* @type {number}
* @default -1
* @private
*/
_idleStart = -1
/**
* The function to be executed when the timer expires.
* @type {Function}
* @private
*/
_onTimeout
/**
* The argument to be passed to the callback when the timer expires.
*
* @type {*}
* @private
*/
_timerArg
/**
* @constructor
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should wait
* before the specified function or code is executed.
* @param {*} arg
*/
constructor (callback, delay, arg) {
this._onTimeout = callback
this._idleTimeout = delay
this._timerArg = arg
this.refresh()
}
/**
* Sets the timer's start time to the current time, and reschedules the timer
* to call its callback at the previously specified duration adjusted to the
* current time.
* Using this on a timer that has already called its callback will reactivate
* the timer.
*
* @returns {void}
*/
refresh () {
if (this.state === -2) {
// In the special case that the timer is not in the list of active timers,
// add it back to the array to be processed in the next tick by the onTick
// function.
if (this._state === NOT_IN_LIST) {
fastTimers.push(this)
if (!fastNowTimeout || fastTimers.length === 1) {
refreshTimeout()
}
}
this.state = 0
// If the timer is the only active timer, refresh the fastNowTimeout for
// better resolution.
if (!fastNowTimeout || fastTimers.length === 1) {
refreshTimeout()
}
// Setting the state to PENDING will cause the timer to be reset in the
// next tick by the onTick function.
this._state = PENDING
}
/**
* The `clear` method cancels the timer, preventing it from executing.
*
* @returns {void}
* @private
*/
clear () {
this.state = -1
// Set the state to TO_BE_CLEARED to mark the timer for removal in the next
// tick by the onTick function.
this._state = TO_BE_CLEARED
// Reset the _idleStart value to -1 to indicate that the timer is no longer
// active.
this._idleStart = -1
}
}
/**
* This module exports a setTimeout and clearTimeout function that can be
* used as a drop-in replacement for the native functions.
*/
module.exports = {
setTimeout (callback, delay, opaque) {
return delay <= 1e3
? setTimeout(callback, delay, opaque)
: new Timeout(callback, delay, opaque)
/**
* The setTimeout() method sets a timer which executes a function once the
* timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {NodeJS.Timeout|FastTimer}
*/
setTimeout (callback, delay, arg) {
// If the delay is less than or equal to the RESOLUTION_MS value return a
// native Node.js Timer instance.
return delay <= RESOLUTION_MS
? setTimeout(callback, delay, arg)
: new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout method cancels an instantiated Timer previously created
* by calling setTimeout.
*
* @param {NodeJS.Timeout|FastTimer} timeout
*/
clearTimeout (timeout) {
if (timeout instanceof Timeout) {
// If the timeout is a FastTimer, call its own clear method.
if (timeout[kFastTimer]) {
/**
* @type {FastTimer}
*/
timeout.clear()
// Otherwise it is an instance of a native NodeJS.Timeout, so call the
// Node.js native clearTimeout function.
} else {
clearTimeout(timeout)
}
}
},
/**
* The setFastTimeout() method sets a fastTimer which executes a function once
* the timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {FastTimer}
*/
setFastTimeout (callback, delay, arg) {
return new FastTimer(callback, delay, arg)
},
/**
* The clearTimeout method cancels an instantiated FastTimer previously
* created by calling setFastTimeout.
*
* @param {FastTimer} timeout
*/
clearFastTimeout (timeout) {
timeout.clear()
},
/**
* The now method returns the value of the internal fast timer clock.
*
* @returns {number}
*/
now () {
return fastNow
},
/**
* Trigger the onTick function to process the fastTimers array.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
* @param {number} [delay=0] The delay in milliseconds to add to the now value.
*/
tick (delay = 0) {
fastNow += delay - RESOLUTION_MS + 1
onTick()
onTick()
},
/**
* Reset FastTimers.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
reset () {
fastNow = 0
fastTimers.length = 0
clearTimeout(fastNowTimeout)
fastNowTimeout = null
},
/**
* Exporting for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
kFastTimer
}
+12 -2
View File
@@ -87,11 +87,21 @@ function multipartFormDataParser (input, mimeType) {
// the first byte.
const position = { position: 0 }
// Note: undici addition, allow \r\n before the body.
if (input[0] === 0x0d && input[1] === 0x0a) {
// Note: undici addition, allows leading and trailing CRLFs.
while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
position.position += 2
}
let trailing = input.length
while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
trailing -= 2
}
if (trailing !== input.length) {
input = input.subarray(0, trailing)
}
// 5. While true:
while (true) {
// 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
+16 -4
View File
@@ -2150,9 +2150,15 @@ async function httpNetworkFetch (
finishFlush: zlib.constants.Z_SYNC_FLUSH
}))
} else if (coding === 'deflate') {
decoders.push(createInflate())
decoders.push(createInflate({
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH
}))
} else if (coding === 'br') {
decoders.push(zlib.createBrotliDecompress())
decoders.push(zlib.createBrotliDecompress({
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}))
} else {
decoders.length = 0
break
@@ -2160,13 +2166,19 @@ async function httpNetworkFetch (
}
}
const onError = this.onError.bind(this)
resolve({
status,
statusText,
headersList,
body: decoders.length
? pipeline(this.body, ...decoders, () => { })
: this.body.on('error', () => { })
? pipeline(this.body, ...decoders, (err) => {
if (err) {
this.onError(err)
}
}).on('error', onError)
: this.body.on('error', onError)
})
return true
+16 -4
View File
@@ -1340,6 +1340,14 @@ function buildContentRange (rangeStart, rangeEnd, fullLength) {
// interpreted as a zlib stream, otherwise it's interpreted as a
// raw deflate stream.
class InflateStream extends Transform {
#zlibOptions
/** @param {zlib.ZlibOptions} [zlibOptions] */
constructor (zlibOptions) {
super()
this.#zlibOptions = zlibOptions
}
_transform (chunk, encoding, callback) {
if (!this._inflateStream) {
if (chunk.length === 0) {
@@ -1347,8 +1355,8 @@ class InflateStream extends Transform {
return
}
this._inflateStream = (chunk[0] & 0x0F) === 0x08
? zlib.createInflate()
: zlib.createInflateRaw()
? zlib.createInflate(this.#zlibOptions)
: zlib.createInflateRaw(this.#zlibOptions)
this._inflateStream.on('data', this.push.bind(this))
this._inflateStream.on('end', () => this.push(null))
@@ -1367,8 +1375,12 @@ class InflateStream extends Transform {
}
}
function createInflate () {
return new InflateStream()
/**
* @param {zlib.ZlibOptions} [zlibOptions]
* @returns {InflateStream}
*/
function createInflate (zlibOptions) {
return new InflateStream(zlibOptions)
}
/**
+284 -271
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "undici",
"version": "6.19.8",
"version": "6.20.0",
"description": "An HTTP/1.1 client, written from scratch for Node.js",
"homepage": "https://undici.nodejs.org",
"bugs": {
@@ -105,7 +105,7 @@
"@fastify/busboy": "2.1.1",
"@matteo.collina/tspl": "^0.1.1",
"@sinonjs/fake-timers": "^11.1.0",
"@types/node": "^18.0.3",
"@types/node": "~18.19.50",
"abort-controller": "^3.0.0",
"borp": "^0.15.0",
"c8": "^10.0.0",
-2
View File
@@ -2,8 +2,6 @@ import { MessageEvent, ErrorEvent } from './websocket'
import Dispatcher from './dispatcher'
import {
EventTarget,
Event,
EventListenerOptions,
AddEventListenerOptions,
EventListenerOrEventListenerObject
+1 -1
View File
@@ -1,7 +1,7 @@
/// <reference types="node" />
import { Blob } from 'buffer'
import { DOMException, Event, EventInit, EventTarget } from './patch'
import { DOMException, EventInit } from './patch'
export declare class FileReader {
__proto__: EventTarget & FileReader
+3 -1
View File
@@ -7,9 +7,11 @@ declare namespace Interceptors {
export type DumpInterceptorOpts = { maxSize?: number }
export type RetryInterceptorOpts = RetryHandler.RetryOptions
export type RedirectInterceptorOpts = { maxRedirections?: number }
export type ResponseErrorInterceptorOpts = { throwOnError: boolean }
export function createRedirectInterceptor(opts: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
export function dump(opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
export function retry(opts?: RetryInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
export function redirect(opts?: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
export function responseError(opts?: ResponseErrorInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
}
-38
View File
@@ -6,44 +6,6 @@ export type DOMException = typeof globalThis extends { DOMException: infer T }
? T
: any
export type EventTarget = typeof globalThis extends { EventTarget: infer T }
? T
: {
addEventListener(
type: string,
listener: any,
options?: any,
): void
dispatchEvent(event: Event): boolean
removeEventListener(
type: string,
listener: any,
options?: any | boolean,
): void
}
export type Event = typeof globalThis extends { Event: infer T }
? T
: {
readonly bubbles: boolean
cancelBubble: () => void
readonly cancelable: boolean
readonly composed: boolean
composedPath(): [EventTarget?]
readonly currentTarget: EventTarget | null
readonly defaultPrevented: boolean
readonly eventPhase: 0 | 2
readonly isTrusted: boolean
preventDefault(): void
returnValue: boolean
readonly srcElement: EventTarget | null
stopImmediatePropagation(): void
stopPropagation(): void
readonly target: EventTarget | null
readonly timeStamp: number
readonly type: string
}
export interface EventInit {
bubbles?: boolean
cancelable?: boolean
-2
View File
@@ -3,8 +3,6 @@
import type { Blob } from 'buffer'
import type { MessagePort } from 'worker_threads'
import {
EventTarget,
Event,
EventInit,
EventListenerOptions,
AddEventListenerOptions,
+366 -159
View File
@@ -247,6 +247,20 @@ var require_errors = __commonJS({
this.headers = headers;
}
};
var ResponseError = class extends UndiciError {
static {
__name(this, "ResponseError");
}
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;
}
};
var SecureProxyConnectionError = class extends UndiciError {
static {
__name(this, "SecureProxyConnectionError");
@@ -281,6 +295,7 @@ var require_errors = __commonJS({
BalancedPoolMissingUpstreamError,
ResponseExceededMaxSizeError,
RequestRetryError,
ResponseError,
SecureProxyConnectionError
};
}
@@ -1266,7 +1281,7 @@ var require_util = __commonJS({
if (!host) {
return null;
}
assert.strictEqual(typeof host, "string");
assert(typeof host === "string");
const servername = getHostname(host);
if (net.isIP(servername)) {
return "";
@@ -2166,6 +2181,242 @@ var require_request = __commonJS({
}
});
// lib/util/timers.js
var require_timers = __commonJS({
"lib/util/timers.js"(exports2, module2) {
"use strict";
var fastNow = 0;
var RESOLUTION_MS = 1e3;
var TICK_MS = (RESOLUTION_MS >> 1) - 1;
var fastNowTimeout;
var kFastTimer = Symbol("kFastTimer");
var fastTimers = [];
var NOT_IN_LIST = -2;
var TO_BE_CLEARED = -1;
var PENDING = 0;
var ACTIVE = 1;
function onTick() {
fastNow += TICK_MS;
let idx = 0;
let len = fastTimers.length;
while (idx < len) {
const timer = fastTimers[idx];
if (timer._state === PENDING) {
timer._idleStart = fastNow - TICK_MS;
timer._state = ACTIVE;
} else if (timer._state === ACTIVE && fastNow >= timer._idleStart + timer._idleTimeout) {
timer._state = TO_BE_CLEARED;
timer._idleStart = -1;
timer._onTimeout(timer._timerArg);
}
if (timer._state === TO_BE_CLEARED) {
timer._state = NOT_IN_LIST;
if (--len !== 0) {
fastTimers[idx] = fastTimers[len];
}
} else {
++idx;
}
}
fastTimers.length = len;
if (fastTimers.length !== 0) {
refreshTimeout();
}
}
__name(onTick, "onTick");
function refreshTimeout() {
if (fastNowTimeout) {
fastNowTimeout.refresh();
} else {
clearTimeout(fastNowTimeout);
fastNowTimeout = setTimeout(onTick, TICK_MS);
if (fastNowTimeout.unref) {
fastNowTimeout.unref();
}
}
}
__name(refreshTimeout, "refreshTimeout");
var FastTimer = class {
static {
__name(this, "FastTimer");
}
[kFastTimer] = true;
/**
* The state of the timer, which can be one of the following:
* - NOT_IN_LIST (-2)
* - TO_BE_CLEARED (-1)
* - PENDING (0)
* - ACTIVE (1)
*
* @type {-2|-1|0|1}
* @private
*/
_state = NOT_IN_LIST;
/**
* The number of milliseconds to wait before calling the callback.
*
* @type {number}
* @private
*/
_idleTimeout = -1;
/**
* The time in milliseconds when the timer was started. This value is used to
* calculate when the timer should expire.
*
* @type {number}
* @default -1
* @private
*/
_idleStart = -1;
/**
* The function to be executed when the timer expires.
* @type {Function}
* @private
*/
_onTimeout;
/**
* The argument to be passed to the callback when the timer expires.
*
* @type {*}
* @private
*/
_timerArg;
/**
* @constructor
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should wait
* before the specified function or code is executed.
* @param {*} arg
*/
constructor(callback, delay, arg) {
this._onTimeout = callback;
this._idleTimeout = delay;
this._timerArg = arg;
this.refresh();
}
/**
* Sets the timer's start time to the current time, and reschedules the timer
* to call its callback at the previously specified duration adjusted to the
* current time.
* Using this on a timer that has already called its callback will reactivate
* the timer.
*
* @returns {void}
*/
refresh() {
if (this._state === NOT_IN_LIST) {
fastTimers.push(this);
}
if (!fastNowTimeout || fastTimers.length === 1) {
refreshTimeout();
}
this._state = PENDING;
}
/**
* The `clear` method cancels the timer, preventing it from executing.
*
* @returns {void}
* @private
*/
clear() {
this._state = TO_BE_CLEARED;
this._idleStart = -1;
}
};
module2.exports = {
/**
* The setTimeout() method sets a timer which executes a function once the
* timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {NodeJS.Timeout|FastTimer}
*/
setTimeout(callback, delay, arg) {
return delay <= RESOLUTION_MS ? setTimeout(callback, delay, arg) : new FastTimer(callback, delay, arg);
},
/**
* The clearTimeout method cancels an instantiated Timer previously created
* by calling setTimeout.
*
* @param {NodeJS.Timeout|FastTimer} timeout
*/
clearTimeout(timeout) {
if (timeout[kFastTimer]) {
timeout.clear();
} else {
clearTimeout(timeout);
}
},
/**
* The setFastTimeout() method sets a fastTimer which executes a function once
* the timer expires.
* @param {Function} callback A function to be executed after the timer
* expires.
* @param {number} delay The time, in milliseconds that the timer should
* wait before the specified function or code is executed.
* @param {*} [arg] An optional argument to be passed to the callback function
* when the timer expires.
* @returns {FastTimer}
*/
setFastTimeout(callback, delay, arg) {
return new FastTimer(callback, delay, arg);
},
/**
* The clearTimeout method cancels an instantiated FastTimer previously
* created by calling setFastTimeout.
*
* @param {FastTimer} timeout
*/
clearFastTimeout(timeout) {
timeout.clear();
},
/**
* The now method returns the value of the internal fast timer clock.
*
* @returns {number}
*/
now() {
return fastNow;
},
/**
* Trigger the onTick function to process the fastTimers array.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
* @param {number} [delay=0] The delay in milliseconds to add to the now value.
*/
tick(delay = 0) {
fastNow += delay - RESOLUTION_MS + 1;
onTick();
onTick();
},
/**
* Reset FastTimers.
* Exported for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
reset() {
fastNow = 0;
fastTimers.length = 0;
clearTimeout(fastNowTimeout);
fastNowTimeout = null;
},
/**
* Exporting for testing purposes only.
* Marking as deprecated to discourage any use outside of testing.
* @deprecated
*/
kFastTimer
};
}
});
// lib/core/connect.js
var require_connect = __commonJS({
"lib/core/connect.js"(exports2, module2) {
@@ -2174,6 +2425,10 @@ var require_connect = __commonJS({
var assert = require("node:assert");
var util = require_util();
var { InvalidArgumentError, ConnectTimeoutError } = require_errors();
var timers = require_timers();
function noop() {
}
__name(noop, "noop");
var tls;
var SessionCache;
if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
@@ -2246,8 +2501,9 @@ var require_connect = __commonJS({
}
servername = servername || options.servername || util.getServerName(host) || null;
const sessionKey = servername || hostname;
const session = customSession || 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...
@@ -2259,7 +2515,7 @@ var require_connect = __commonJS({
ALPNProtocols: allowH2 ? ["http/1.1", "h2"] : ["http/1.1"],
socket: httpSocket,
// upgrade socket connection
port: port || 443,
port,
host: hostname
});
socket.on("session", function(session2) {
@@ -2267,12 +2523,13 @@ var require_connect = __commonJS({
});
} 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
});
}
@@ -2280,16 +2537,16 @@ var require_connect = __commonJS({
const keepAliveInitialDelay = options.keepAliveInitialDelay === void 0 ? 6e4 : options.keepAliveInitialDelay;
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;
callback = null;
cb(null, this);
}
}).on("error", function(err) {
cancelTimeout();
queueMicrotask(clearConnectTimeout);
if (callback) {
const cb = callback;
callback = null;
@@ -2300,34 +2557,45 @@ var require_connect = __commonJS({
}, "connect");
}
__name(buildConnector, "buildConnector");
function setupTimeout(onConnectTimeout2, timeout) {
if (!timeout) {
return () => {
};
var setupConnectTimeout = process.platform === "win32" ? (socketWeakRef, opts) => {
if (!opts.timeout) {
return noop;
}
let s1 = null;
let s2 = null;
const timeoutId = setTimeout(() => {
const fastTimer = timers.setFastTimeout(() => {
s1 = setImmediate(() => {
if (process.platform === "win32") {
s2 = setImmediate(() => onConnectTimeout2());
} else {
onConnectTimeout2();
}
s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts));
});
}, timeout);
}, opts.timeout);
return () => {
clearTimeout(timeoutId);
timers.clearFastTimeout(fastTimer);
clearImmediate(s1);
clearImmediate(s2);
};
}
__name(setupTimeout, "setupTimeout");
function onConnectTimeout(socket) {
} : (socketWeakRef, opts) => {
if (!opts.timeout) {
return noop;
}
let s1 = null;
const fastTimer = timers.setFastTimeout(() => {
s1 = setImmediate(() => {
onConnectTimeout(socketWeakRef.deref(), opts);
});
}, opts.timeout);
return () => {
timers.clearFastTimeout(fastTimer);
clearImmediate(s1);
};
};
function onConnectTimeout(socket, opts) {
let message = "Connect Timeout Error";
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(", ")})`;
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));
}
__name(onConnectTimeout, "onConnectTimeout");
@@ -2335,94 +2603,6 @@ var require_connect = __commonJS({
}
});
// lib/util/timers.js
var require_timers = __commonJS({
"lib/util/timers.js"(exports2, module2) {
"use strict";
var TICK_MS = 499;
var fastNow = Date.now();
var fastNowTimeout;
var fastTimers = [];
function onTimeout() {
fastNow = Date.now();
let len = fastTimers.length;
let idx = 0;
while (idx < len) {
const timer = fastTimers[idx];
if (timer.state === 0) {
timer.state = fastNow + timer.delay - TICK_MS;
} else if (timer.state > 0 && fastNow >= timer.state) {
timer.state = -1;
timer.callback(timer.opaque);
}
if (timer.state === -1) {
timer.state = -2;
if (idx !== len - 1) {
fastTimers[idx] = fastTimers.pop();
} else {
fastTimers.pop();
}
len -= 1;
} else {
idx += 1;
}
}
if (fastTimers.length > 0) {
refreshTimeout();
}
}
__name(onTimeout, "onTimeout");
function refreshTimeout() {
if (fastNowTimeout?.refresh) {
fastNowTimeout.refresh();
} else {
clearTimeout(fastNowTimeout);
fastNowTimeout = setTimeout(onTimeout, TICK_MS);
if (fastNowTimeout.unref) {
fastNowTimeout.unref();
}
}
}
__name(refreshTimeout, "refreshTimeout");
var Timeout = class {
static {
__name(this, "Timeout");
}
constructor(callback, delay, opaque) {
this.callback = callback;
this.delay = delay;
this.opaque = opaque;
this.state = -2;
this.refresh();
}
refresh() {
if (this.state === -2) {
fastTimers.push(this);
if (!fastNowTimeout || fastTimers.length === 1) {
refreshTimeout();
}
}
this.state = 0;
}
clear() {
this.state = -1;
}
};
module2.exports = {
setTimeout(callback, delay, opaque) {
return delay <= 1e3 ? setTimeout(callback, delay, opaque) : new Timeout(callback, delay, opaque);
},
clearTimeout(timeout) {
if (timeout instanceof Timeout) {
timeout.clear();
} else {
clearTimeout(timeout);
}
}
};
}
});
// lib/llhttp/utils.js
var require_utils = __commonJS({
"lib/llhttp/utils.js"(exports2) {
@@ -4523,13 +4703,19 @@ var require_util2 = __commonJS({
static {
__name(this, "InflateStream");
}
#zlibOptions;
/** @param {zlib.ZlibOptions} [zlibOptions] */
constructor(zlibOptions) {
super();
this.#zlibOptions = zlibOptions;
}
_transform(chunk, encoding, callback) {
if (!this._inflateStream) {
if (chunk.length === 0) {
callback();
return;
}
this._inflateStream = (chunk[0] & 15) === 8 ? zlib.createInflate() : zlib.createInflateRaw();
this._inflateStream = (chunk[0] & 15) === 8 ? zlib.createInflate(this.#zlibOptions) : zlib.createInflateRaw(this.#zlibOptions);
this._inflateStream.on("data", this.push.bind(this));
this._inflateStream.on("end", () => this.push(null));
this._inflateStream.on("error", (err) => this.destroy(err));
@@ -4544,8 +4730,8 @@ var require_util2 = __commonJS({
callback();
}
};
function createInflate() {
return new InflateStream();
function createInflate(zlibOptions) {
return new InflateStream(zlibOptions);
}
__name(createInflate, "createInflate");
function extractMimeType(headers) {
@@ -4984,9 +5170,16 @@ var require_formdata_parser = __commonJS({
const boundary = Buffer.from(`--${boundaryString}`, "utf8");
const entryList = [];
const position = { position: 0 };
if (input[0] === 13 && input[1] === 10) {
while (input[position.position] === 13 && input[position.position + 1] === 10) {
position.position += 2;
}
let trailing = input.length;
while (input[trailing - 1] === 10 && input[trailing - 2] === 13) {
trailing -= 2;
}
if (trailing !== input.length) {
input = input.subarray(0, trailing);
}
while (true) {
if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
position.position += boundary.length;
@@ -5584,35 +5777,35 @@ var require_client_h1 = __commonJS({
return 0;
},
wasm_on_status: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
const start = at - currentBufferPtr + currentBufferRef.byteOffset;
return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0;
},
wasm_on_message_begin: (p) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
return currentParser.onMessageBegin() || 0;
},
wasm_on_header_field: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
const start = at - currentBufferPtr + currentBufferRef.byteOffset;
return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0;
},
wasm_on_header_value: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
const start = at - currentBufferPtr + currentBufferRef.byteOffset;
return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0;
},
wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0;
},
wasm_on_body: (p, at, len) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
const start = at - currentBufferPtr + currentBufferRef.byteOffset;
return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0;
},
wasm_on_message_complete: (p) => {
assert.strictEqual(currentParser.ptr, p);
assert(currentParser.ptr === p);
return currentParser.onMessageComplete() || 0;
}
/* eslint-enable camelcase */
@@ -5627,9 +5820,11 @@ var require_client_h1 = __commonJS({
var currentBufferRef = null;
var currentBufferSize = 0;
var currentBufferPtr = null;
var TIMEOUT_HEADERS = 1;
var TIMEOUT_BODY = 2;
var TIMEOUT_IDLE = 3;
var USE_NATIVE_TIMER = 0;
var USE_FAST_TIMER = 1;
var TIMEOUT_HEADERS = 2 | USE_FAST_TIMER;
var TIMEOUT_BODY = 4 | USE_FAST_TIMER;
var TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER;
var Parser = class {
static {
__name(this, "Parser");
@@ -5658,24 +5853,27 @@ var require_client_h1 = __commonJS({
this.connection = "";
this.maxResponseSize = client[kMaxResponseSize];
}
setTimeout(value, type) {
this.timeoutType = type;
if (value !== this.timeoutValue) {
timers.clearTimeout(this.timeout);
if (value) {
this.timeout = timers.setTimeout(onParserTimeout, value, this);
if (this.timeout.unref) {
this.timeout.unref();
}
} else {
setTimeout(delay, type) {
if (delay !== this.timeoutValue || type & USE_FAST_TIMER ^ this.timeoutType & USE_FAST_TIMER) {
if (this.timeout) {
timers.clearTimeout(this.timeout);
this.timeout = null;
}
this.timeoutValue = value;
if (delay) {
if (type & USE_FAST_TIMER) {
this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this));
} else {
this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this));
this.timeout.unref();
}
}
this.timeoutValue = delay;
} else if (this.timeout) {
if (this.timeout.refresh) {
this.timeout.refresh();
}
}
this.timeoutType = type;
}
resume() {
if (this.socket.destroyed || !this.paused) {
@@ -5752,7 +5950,7 @@ var require_client_h1 = __commonJS({
assert(currentParser == null);
this.llhttp.llhttp_free(this.ptr);
this.ptr = null;
timers.clearTimeout(this.timeout);
this.timeout && timers.clearTimeout(this.timeout);
this.timeout = null;
this.timeoutValue = null;
this.timeoutType = null;
@@ -5811,16 +6009,16 @@ var require_client_h1 = __commonJS({
onUpgrade(head) {
const { upgrade, client, socket, headers, statusCode } = this;
assert(upgrade);
assert(client[kSocket] === socket);
assert(!socket.destroyed);
assert(!this.paused);
assert((headers.length & 1) === 0);
const request = client[kQueue][client[kRunningIdx]];
assert(request);
assert(!socket.destroyed);
assert(socket === client[kSocket]);
assert(!this.paused);
assert(request.upgrade || request.method === "CONNECT");
this.statusCode = null;
this.statusText = "";
this.shouldKeepAlive = null;
assert(this.headers.length % 2 === 0);
this.headers = [];
this.headersSize = 0;
socket.unshift(head);
@@ -5859,7 +6057,7 @@ var require_client_h1 = __commonJS({
util.destroy(socket, new SocketError("bad upgrade", util.getSocketInfo(socket)));
return -1;
}
assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS);
assert(this.timeoutType === TIMEOUT_HEADERS);
this.statusCode = statusCode;
this.shouldKeepAlive = shouldKeepAlive || // Override llhttp value which does not allow keepAlive for HEAD.
request.method === "HEAD" && !socket[kReset] && this.connection.toLowerCase() === "keep-alive";
@@ -5881,7 +6079,7 @@ var require_client_h1 = __commonJS({
this.upgrade = true;
return 2;
}
assert(this.headers.length % 2 === 0);
assert((this.headers.length & 1) === 0);
this.headers = [];
this.headersSize = 0;
if (this.shouldKeepAlive && client[kPipelining]) {
@@ -5925,7 +6123,7 @@ var require_client_h1 = __commonJS({
}
const request = client[kQueue][client[kRunningIdx]];
assert(request);
assert.strictEqual(this.timeoutType, TIMEOUT_BODY);
assert(this.timeoutType === TIMEOUT_BODY);
if (this.timeout) {
if (this.timeout.refresh) {
this.timeout.refresh();
@@ -5949,16 +6147,16 @@ var require_client_h1 = __commonJS({
if (upgrade) {
return;
}
assert(statusCode >= 100);
assert((this.headers.length & 1) === 0);
const request = client[kQueue][client[kRunningIdx]];
assert(request);
assert(statusCode >= 100);
this.statusCode = null;
this.statusText = "";
this.bytesRead = 0;
this.contentLength = "";
this.keepAlive = "";
this.connection = "";
assert(this.headers.length % 2 === 0);
this.headers = [];
this.headersSize = 0;
if (statusCode < 200) {
@@ -5971,7 +6169,7 @@ var require_client_h1 = __commonJS({
request.onComplete(headers);
client[kQueue][client[kRunningIdx]++] = null;
if (socket[kWriting]) {
assert.strictEqual(client[kRunning], 0);
assert(client[kRunning] === 0);
util.destroy(socket, new InformationalError("reset"));
return constants.ERROR.PAUSED;
} else if (!shouldKeepAlive) {
@@ -5988,17 +6186,17 @@ var require_client_h1 = __commonJS({
}
};
function onParserTimeout(parser) {
const { socket, timeoutType, client } = parser;
const { socket, timeoutType, client, paused } = parser.deref();
if (timeoutType === TIMEOUT_HEADERS) {
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
assert(!parser.paused, "cannot be paused while waiting for headers");
assert(!paused, "cannot be paused while waiting for headers");
util.destroy(socket, new HeadersTimeoutError());
}
} else if (timeoutType === TIMEOUT_BODY) {
if (!parser.paused) {
if (!paused) {
util.destroy(socket, new BodyTimeoutError());
}
} else if (timeoutType === TIMEOUT_IDLE) {
} else if (timeoutType === TIMEOUT_KEEP_ALIVE) {
assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]);
util.destroy(socket, new InformationalError("socket idle timeout"));
}
@@ -6016,8 +6214,8 @@ var require_client_h1 = __commonJS({
socket[kBlocking] = false;
socket[kParser] = new Parser(client, socket, llhttpInstance);
addListener(socket, "error", function(err) {
const parser = this[kParser];
assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID");
const parser = this[kParser];
if (err.code === "ECONNRESET" && parser.statusCode && !parser.shouldKeepAlive) {
parser.onMessageComplete();
return;
@@ -6125,8 +6323,8 @@ var require_client_h1 = __commonJS({
socket[kNoRef] = false;
}
if (client[kSize] === 0) {
if (socket[kParser].timeoutType !== TIMEOUT_IDLE) {
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE);
if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) {
socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE);
}
} else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) {
if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) {
@@ -10889,22 +11087,31 @@ var require_fetch = __commonJS({
finishFlush: zlib.constants.Z_SYNC_FLUSH
}));
} else if (coding === "deflate") {
decoders.push(createInflate());
decoders.push(createInflate({
flush: zlib.constants.Z_SYNC_FLUSH,
finishFlush: zlib.constants.Z_SYNC_FLUSH
}));
} else if (coding === "br") {
decoders.push(zlib.createBrotliDecompress());
decoders.push(zlib.createBrotliDecompress({
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
}));
} else {
decoders.length = 0;
break;
}
}
}
const onError = this.onError.bind(this);
resolve({
status,
statusText,
headersList,
body: decoders.length ? pipeline(this.body, ...decoders, () => {
}) : this.body.on("error", () => {
})
body: decoders.length ? pipeline(this.body, ...decoders, (err) => {
if (err) {
this.onError(err);
}
}).on("error", onError) : this.body.on("error", onError)
});
return true;
},
+1 -1
View File
@@ -2,5 +2,5 @@
// Refer to tools/dep_updaters/update-undici.sh
#ifndef SRC_UNDICI_VERSION_H_
#define SRC_UNDICI_VERSION_H_
#define UNDICI_VERSION "6.19.8"
#define UNDICI_VERSION "6.20.0"
#endif // SRC_UNDICI_VERSION_H_