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
17 changes: 15 additions & 2 deletions deps/undici/src/docs/docs/api/Interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,21 @@ backing store.

**Parameters**

* `opts` {CacheHandler.CacheOptions} (optional) See the
[`CacheStore`](./CacheStore.md) documentation for accepted fields.
* `opts` {Object} (optional)
* `store` {CacheStore} The store that cached responses are kept in. See
[`CacheStore`](./CacheStore.md). **Default:** a new `MemoryCacheStore`.
* `methods` {string[]} The HTTP methods to cache. Only safe methods are
allowed (`GET`, `HEAD`, `OPTIONS`, `TRACE`) and at least one is required.
**Default:** `['GET']`.
* `cacheByDefault` {number} RFC 9111 allows caching responses that are not
explicitly marked as cacheable or not cacheable. Set this to cache such
responses. **Default:** `undefined`.
* `type` {string} Whether the cache is `'shared'` or `'private'`, which
decides how directives such as `s-maxage` and `private` are applied.
**Default:** `'shared'`.
* `origins` {(string|RegExp)[]} Only requests to these origins are cached.
Strings are compared case-insensitively. **Default:** `undefined` (all
origins are cached).

**Returns:** {Dispatcher.DispatcherComposeInterceptor}

Expand Down
9 changes: 7 additions & 2 deletions deps/undici/src/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@ const neo = require('neostandard')
const { installedExports } = require('./lib/global')

module.exports = [
...neo({
// neostandard 0.14.0 scopes its `ignores` to its own rule layers, so they no
// longer exclude these generated/vendored files from linting entirely.
// Declare them as global ignores so ESLint skips them outright.
{
ignores: [
'deps/milo',
'lib/milo',
'lib/llhttp',
'test/fixtures/cache-tests',
'undici-fetch.js',
'test/web-platform-tests/wpt'
],
]
},
...neo({
noJsx: true,
ts: true
}),
Expand Down
81 changes: 78 additions & 3 deletions deps/undici/src/lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const kRefusedStreamRetry = Symbol('refused stream retry')
// event loop.
const MAX_GOAWAY_REPLAY_ATTEMPTS = 1

// Lower bound on how long a liveness PING may take to be acked. Stream
// timeouts can be tiny (tens of ms), while an ack on a live connection can take
// longer than that on a loaded event loop; without a floor a slow but healthy
// connection would be torn down.
const MIN_PROBE_TIMEOUT = 1e3

let extractBody

/** @type {import('http2')} */
Expand Down Expand Up @@ -277,6 +283,9 @@ function connectH2 (client, socket) {
// Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have
// work that cannot start. See setNoStreamsTimeout.
noStreamsTimeout: null,
// Armed while a PING checks whether the peer is still there after a
// stream timed out. See probeHttp2Session.
probeTimeout: null,
// Sockets start out ref'd. Session ref/unref proxies to the socket, so a
// single cached flag lets us skip redundant uv ref/unref calls, provided
// every ref/unref of the session or its socket goes through
Expand Down Expand Up @@ -361,6 +370,11 @@ function connectH2 (client, socket) {
return true
}

// Do not open streams on a connection that may be dead.
if (session[kHTTP2SessionState].probeTimeout != null) {
return true
}

if (client[kRunning] >= client[kMaxConcurrentStreams]) {
return true
}
Expand Down Expand Up @@ -494,6 +508,60 @@ function onNoStreamsTimeout (session) {
resetHttp2Session(session, err)
}

// A stream timeout only resets that stream, unlike HTTP/1.1 which destroys the
// socket. If the connection itself silently died (dropped by a NAT or load
// balancer, a hung peer), every new stream would time out in turn and the
// session would never be replaced. Ask the peer for a PING ack within the
// timeout that just fired (but at least MIN_PROBE_TIMEOUT): if it answers, only
// that request was slow; if not, drop the session so queued requests move to a
// fresh connection.
function probeHttp2Session (session, timeout) {
const state = session[kHTTP2SessionState]

if (state.probeTimeout != null || session.closed || session.destroyed) {
return
}

try {
session.ping(onHttp2ProbeAck.bind(session))
} catch {
// The session is going away on its own.
return
}

state.probeTimeout = setTimeout(onHttp2ProbeTimeout, Math.max(timeout, MIN_PROBE_TIMEOUT), session).unref()
}

function clearHttp2ProbeTimeout (session) {
const state = session[kHTTP2SessionState]

if (state?.probeTimeout != null) {
clearTimeout(state.probeTimeout)
state.probeTimeout = null
}
}

function onHttp2ProbeAck () {
if (this[kHTTP2SessionState].probeTimeout == null) {
return
}

clearHttp2ProbeTimeout(this)
this[kClient][kResume]()
}

function onHttp2ProbeTimeout (session) {
session[kHTTP2SessionState].probeTimeout = null

if (session.closed || session.destroyed) {
return
}

const err = new SocketError('HTTP/2: connection unresponsive', util.getSocketInfo(session[kSocket]))
session[kError] = err
resetHttp2Session(session, err)
}

function clearHttp2IdleTimeout (session) {
const state = session[kHTTP2SessionState]

Expand Down Expand Up @@ -686,6 +754,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {

clearHttp2IdleTimeout(this)
clearNoStreamsTimeout(this)
clearHttp2ProbeTimeout(this)

if (!this.closed && !this.destroyed) {
this.close()
Expand All @@ -711,6 +780,7 @@ function onHttp2SessionClose () {

clearHttp2IdleTimeout(this)
clearNoStreamsTimeout(this)
clearHttp2ProbeTimeout(this)

if (state.ping.interval != null) {
clearInterval(state.ping.interval)
Expand Down Expand Up @@ -1610,9 +1680,14 @@ function onTimeout () {
// Remove self so timeout doesn't fire again after we handle it
stream.off('timeout', onTimeout)

const err = state.responseReceived
? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`)
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${state.headersTimeout}"`)
const { responseReceived, session, bodyTimeout, headersTimeout } = state
const err = responseReceived
? new BodyTimeoutError(`HTTP/2: "stream timeout after ${bodyTimeout}"`)
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)

// Probe before aborting: completing the request resumes the queue, which
// must not open new streams until the peer has proven it is alive.
probeHttp2Session(session, responseReceived ? bodyTimeout : headersTimeout)
state.abort(err)
}

Expand Down
23 changes: 20 additions & 3 deletions deps/undici/src/lib/dispatcher/pool-base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const { AsyncResource } = require('node:async_hooks')
const { PoolStats } = require('../util/stats.js')
const DispatcherBase = require('./dispatcher-base')
const FixedQueue = require('./fixed-queue')
Expand All @@ -26,6 +27,22 @@ const kRetiring = Symbol('retiring clients')
// Closes every live client, including clients that have been
// taken out of rotation but are still finishing their own
// requests.
// Requests that wait in the Pool queue are dispatched later from whichever
// callback drains the queue (another request's connect, drain, ...). Capture
// the caller's async context so Client#dispatch, and the diagnostics channel
// events it publishes, still run in it.
class QueuedRequest extends AsyncResource {
constructor (opts, handler) {
super('UNDICI_POOL_QUEUED_REQUEST')
this.opts = opts
this.handler = handler
}

dispatch (dispatcher) {
return this.runInAsyncScope(dispatcher.dispatch, dispatcher, this.opts, this.handler)
}
}

function closeClients (pool) {
const closeAll = []
for (let i = 0; i < pool[kClients].length; i++) {
Expand Down Expand Up @@ -69,7 +86,7 @@ class PoolBase extends DispatcherBase {
break
}
this[kQueued]--
needDrain = !client.dispatch(item.opts, item.handler)
needDrain = !item.dispatch(client)
}

client[kNeedDrain] = needDrain
Expand Down Expand Up @@ -114,7 +131,7 @@ class PoolBase extends DispatcherBase {
const item = queue.shift()
this[kQueued]--

if (!dispatcher.dispatch(item.opts, item.handler)) {
if (!item.dispatch(dispatcher)) {
dispatcher[kNeedDrain] = true
this[kOnClientBusy](dispatcher)
hasDispatcher = this[kHasDispatcher]()
Expand Down Expand Up @@ -228,7 +245,7 @@ class PoolBase extends DispatcherBase {

if (!dispatcher) {
this[kNeedDrain] = true
this[kQueue].push({ opts, handler })
this[kQueue].push(new QueuedRequest(opts, handler))
this[kQueued]++
} else if (!dispatcher.dispatch(opts, handler)) {
dispatcher[kNeedDrain] = true
Expand Down
Loading
Loading