Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deps/undici/src/docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Returns: `Client`
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
* **eventSource** `EventSourceOptions` (optional) - EventSource-specific configuration options.
* **maxEventSize** `number` (optional) - The maximum total bytes of data and metadata field values retained for a pending EventSource event. Each unfinished line is separately limited to this value plus seven syntax bytes. The number of `data:` lines per event and input chunks buffered for an unfinished line are each limited to `min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead. Set to `0` to disable these limits. Default: `buffer.kStringMaxLength`.
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
Expand Down
4 changes: 3 additions & 1 deletion deps/undici/src/docs/docs/api/DiagnosticsChannel.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) =>

## `undici:request:headers`

This message is published after the response headers have been received.
This message is published after the response headers have been received. This includes a successful CONNECT or
protocol upgrade response.

```js
import diagnosticsChannel from 'diagnostics_channel'
Expand Down Expand Up @@ -82,6 +83,7 @@ diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ requ
## `undici:request:trailers`

This message is published after the response body and trailers have been received, i.e. the response has been completed.
After an upgraded socket is passed to the request handler, this message is published with an empty `trailers` array.

```js
import diagnosticsChannel from 'diagnostics_channel'
Expand Down
1 change: 1 addition & 0 deletions deps/undici/src/docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@ The `decompress` interceptor automatically decompresses response bodies that are

- `skipErrorResponses` - Whether to skip decompression for error responses (status codes >= 400). Default: `true`.
- `skipStatusCodes` - Array of status codes to skip decompression for. Default: `[204, 304]`.
- `maxSize` - Maximum decompressed response size in bytes for each decompression stage. Set to `0` to disable the limit. Default: `0`.

**Example - Basic Decompress Interceptor**

Expand Down
14 changes: 10 additions & 4 deletions deps/undici/src/lib/core/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,22 @@ const SessionCache = class WeakSessionCache {
if (this._sessionCache.has(sessionKey)) {
this._sessionCache.delete(sessionKey)
} else if (this._sessionCache.size >= this._maxCachedSessions) {
// Make room for the new session: prefer an entry whose session has
// already been collected, otherwise evict the oldest one.
let evicted = false
for (const [key, ref] of this._sessionCache) {
if (ref.deref() === undefined) {
this._sessionCache.delete(key)
return
evicted = true
break
}
}

const oldest = this._sessionCache.keys().next()
if (!oldest.done) {
this._sessionCache.delete(oldest.value)
if (!evicted) {
const oldest = this._sessionCache.keys().next()
if (!oldest.done) {
this._sessionCache.delete(oldest.value)
}
}
}

Expand Down
25 changes: 23 additions & 2 deletions deps/undici/src/lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,32 @@ class Request {
}
}

onUpgrade (statusCode, headers, socket) {
/**
* @param {number} statusCode
* @param {Buffer[]|string[]} headers
* @param {import('node:stream').Duplex} socket
* @param {string} [statusText]
*/
onUpgrade (statusCode, headers, socket, statusText = '') {
this.onFinally()

assert(!this.aborted)
assert(!this.completed)

return this[kHandler].onUpgrade(statusCode, headers, socket)
if (channels.headers.hasSubscribers) {
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
}

const result = this[kHandler].onUpgrade(statusCode, headers, socket)

if (!this.aborted) {
this.completed = true
if (channels.trailers.hasSubscribers) {
channels.trailers.publish({ request: this, trailers: [] })
}
}

return result
}

onComplete (trailers) {
Expand Down
1 change: 1 addition & 0 deletions deps/undici/src/lib/core/symbols.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
kDestroy: Symbol('destroy'),
kDispatch: Symbol('dispatch'),
kUrl: Symbol('url'),
kRedirectCacheControl: Symbol('redirect cache control'),
kWriting: Symbol('writing'),
kResuming: Symbol('resuming'),
kQueue: Symbol('queue'),
Expand Down
8 changes: 6 additions & 2 deletions deps/undici/src/lib/dispatcher/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function defaultFactory (origin, opts) {
}

class Agent extends DispatcherBase {
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) {
if (typeof factory !== 'function') {
throw new InvalidArgumentError('factory must be a function.')
}
Expand All @@ -41,7 +41,11 @@ class Agent extends DispatcherBase {
connect = { ...connect }
}

this[kOptions] = { ...util.deepClone(options), maxOrigins, connect }
if (tls && typeof tls !== 'function') {
tls = { ...tls }
}

this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls }
this[kFactory] = factory
this[kClients] = new Map()
this[kOrigins] = new Set()
Expand Down
17 changes: 11 additions & 6 deletions deps/undici/src/lib/dispatcher/client-h1.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ function lazyllhttp () {

let mod

// We disable wasm SIMD on ppc64 as it seems to be broken on Power 9 architectures.
let useWasmSIMD = process.arch !== 'ppc64'
// We disable wasm SIMD on older versions of Node.js on ppc64 that are broken on Power >=9 architectures.
let useWasmSIMD = true
if (process.arch === 'ppc64') {
const [major, minor] = process.versions.node.split('.').map(n => parseInt(n, 10))
if (major < 24 || (major === 24 && minor < 12)) {
useWasmSIMD = false
}
}
// The Env Variable UNDICI_NO_WASM_SIMD allows explicitly overriding the default behavior
if (process.env.UNDICI_NO_WASM_SIMD === '1') {
useWasmSIMD = false
Expand Down Expand Up @@ -521,7 +527,7 @@ class Parser {
* @param {Buffer} head
*/
onUpgrade (head) {
const { upgrade, client, socket, headers, statusCode } = this
const { upgrade, client, socket, headers, statusCode, statusText } = this

assert(upgrade)
assert(client[kSocket] === socket)
Expand Down Expand Up @@ -556,8 +562,9 @@ class Parser {
client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade'))

try {
request.onUpgrade(statusCode, headers, socket)
request.onUpgrade(statusCode, headers, socket, statusText)
} catch (err) {
util.errorRequest(client, request, err)
util.destroy(socket, err)
}

Expand Down Expand Up @@ -921,8 +928,6 @@ function connectH1 (client, socket) {
}

function onHttpSocketError (err) {
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
Expand Down
23 changes: 14 additions & 9 deletions deps/undici/src/lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,6 @@ function onHttp2SendPing (session) {
}

function onHttp2SessionError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')

this[kSocket][kError] = err
this[kClient][kOnError](err)
}
Expand Down Expand Up @@ -436,8 +434,6 @@ function onHttp2SocketClose () {
}

function onHttp2SocketError (err) {
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')

this[kError] = err

this[kClient][kOnError](err)
Expand Down Expand Up @@ -575,13 +571,17 @@ function writeH2 (client, request) {

stream = session.request(headers, { endStream: false, signal })
stream[kHTTP2Stream] = true
++session[kOpenStreams]

stream.once('response', (headers, _flags) => {
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers

request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)

++session[kOpenStreams]
try {
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
} catch (err) {
abort(err)
return
}
completeRequest(client, request)
})

Expand Down Expand Up @@ -610,11 +610,16 @@ function writeH2 (client, request) {
// We disabled endStream to allow the user to write to the stream
stream = session.request(headers, { endStream: false, signal })
stream[kHTTP2Stream] = true
++session[kOpenStreams]
stream.on('response', headers => {
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers

request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
++session[kOpenStreams]
try {
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
} catch (err) {
abort(err)
return
}
completeRequest(client, request)
})
stream.on('error', abort)
Expand Down
5 changes: 3 additions & 2 deletions deps/undici/src/lib/dispatcher/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ class Client extends DispatcherBase {
initialWindowSize,
connectionWindowSize,
pingInterval,
webSocket
webSocket,
eventSource
} = {}) {
if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
Expand Down Expand Up @@ -223,7 +224,7 @@ class Client extends DispatcherBase {
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
}

super({ webSocket })
super({ webSocket, eventSource })

if (typeof connect !== 'function') {
connect = buildConnector({
Expand Down
12 changes: 12 additions & 0 deletions deps/undici/src/lib/dispatcher/dispatcher-base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const buffer = require('node:buffer')
const Dispatcher = require('./dispatcher')
const UnwrapHandler = require('../handler/unwrap-handler')
const {
Expand All @@ -12,6 +13,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy
const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed')
const kWebSocketOptions = Symbol('webSocketOptions')
const kEventSourceOptions = Symbol('eventSourceOptions')

class DispatcherBase extends Dispatcher {
/** @type {boolean} */
Expand All @@ -32,6 +34,7 @@ class DispatcherBase extends Dispatcher {
constructor (opts) {
super()
this[kWebSocketOptions] = opts?.webSocket ?? {}
this[kEventSourceOptions] = opts?.eventSource ?? {}
}

/**
Expand All @@ -44,6 +47,15 @@ class DispatcherBase extends Dispatcher {
}
}

/**
* @returns {import('../../types/client').Client.EventSourceOptions}
*/
get eventSourceOptions () {
return {
maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
}
}

/** @returns {boolean} */
get destroyed () {
return this[kDestroyed]
Expand Down
48 changes: 48 additions & 0 deletions deps/undici/src/lib/handler/cache-handler.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

const util = require('../core/util')
const { kRedirectCacheControl } = require('../core/symbols')
const {
parseCacheControlHeader,
hasInvalidCacheControlDirective,
Expand All @@ -13,6 +14,19 @@ const { parseHttpDate } = require('../util/date.js')

function noop () {}

/**
* @param {string | URL} a
* @param {string | URL} b
* @returns {boolean}
*/
function sameOrigin (a, b) {
try {
return new URL(String(a)).origin === new URL(String(b)).origin
} catch {
return String(a) === String(b)
}
}

// Status codes that we can use some heuristics on to cache
const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
Expand Down Expand Up @@ -151,6 +165,14 @@ class CacheHandler {
*/
#writeStream

/**
* The URLs visited while following redirects, shared by reference from the
* redirect handler through the request context. Carries the suppressed hops'
* cache-control under {@link kRedirectCacheControl}.
* @type {URL[] | undefined}
*/
#history

/**
* @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
* @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
Expand All @@ -167,6 +189,9 @@ class CacheHandler {
onRequestStart (controller, context) {
this.#writeStream?.destroy()
this.#writeStream = undefined
// The redirect handler mutates this shared history as redirects are
// followed, allowing the cache to identify the response's actual origin.
this.#history = context?.history
this.#handler.onRequestStart?.(controller, context)
}

Expand Down Expand Up @@ -206,6 +231,29 @@ class CacheHandler {
return downstreamOnHeaders()
}

// Never store a response produced by a different origin under the original
// request's cache key.
if (this.#history && this.#history.length > 0) {
const finalUrl = this.#history[this.#history.length - 1]
if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
return downstreamOnHeaders()
}
}

// Preserve restrictive cache policy from suppressed redirect responses.
const redirectCacheControl = this.#history?.[kRedirectCacheControl]
if (redirectCacheControl) {
for (const hopCacheControl of redirectCacheControl) {
const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
if (
hopDirectives['no-store'] === true ||
(this.#cacheType === 'shared' && hopDirectives.private === true)
) {
return downstreamOnHeaders()
}
}
}

const cacheControlHeader = resHeaders['cache-control']
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}

Expand Down
Loading
Loading