undici 8.12.2 → 8.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Interceptors.md +15 -2
- package/lib/dispatcher/client-h2.js +78 -3
- package/lib/dispatcher/pool-base.js +20 -3
- package/package.json +3 -3
- package/types/api.d.ts +1 -1
- package/types/utility.d.ts +4 -4
|
@@ -341,8 +341,21 @@ backing store.
|
|
|
341
341
|
|
|
342
342
|
**Parameters**
|
|
343
343
|
|
|
344
|
-
* `opts` {
|
|
345
|
-
|
|
344
|
+
* `opts` {Object} (optional)
|
|
345
|
+
* `store` {CacheStore} The store that cached responses are kept in. See
|
|
346
|
+
[`CacheStore`](./CacheStore.md). **Default:** a new `MemoryCacheStore`.
|
|
347
|
+
* `methods` {string[]} The HTTP methods to cache. Only safe methods are
|
|
348
|
+
allowed (`GET`, `HEAD`, `OPTIONS`, `TRACE`) and at least one is required.
|
|
349
|
+
**Default:** `['GET']`.
|
|
350
|
+
* `cacheByDefault` {number} RFC 9111 allows caching responses that are not
|
|
351
|
+
explicitly marked as cacheable or not cacheable. Set this to cache such
|
|
352
|
+
responses. **Default:** `undefined`.
|
|
353
|
+
* `type` {string} Whether the cache is `'shared'` or `'private'`, which
|
|
354
|
+
decides how directives such as `s-maxage` and `private` are applied.
|
|
355
|
+
**Default:** `'shared'`.
|
|
356
|
+
* `origins` {(string|RegExp)[]} Only requests to these origins are cached.
|
|
357
|
+
Strings are compared case-insensitively. **Default:** `undefined` (all
|
|
358
|
+
origins are cached).
|
|
346
359
|
|
|
347
360
|
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
|
|
348
361
|
|
|
@@ -61,6 +61,12 @@ const kRefusedStreamRetry = Symbol('refused stream retry')
|
|
|
61
61
|
// event loop.
|
|
62
62
|
const MAX_GOAWAY_REPLAY_ATTEMPTS = 1
|
|
63
63
|
|
|
64
|
+
// Lower bound on how long a liveness PING may take to be acked. Stream
|
|
65
|
+
// timeouts can be tiny (tens of ms), while an ack on a live connection can take
|
|
66
|
+
// longer than that on a loaded event loop; without a floor a slow but healthy
|
|
67
|
+
// connection would be torn down.
|
|
68
|
+
const MIN_PROBE_TIMEOUT = 1e3
|
|
69
|
+
|
|
64
70
|
let extractBody
|
|
65
71
|
|
|
66
72
|
/** @type {import('http2')} */
|
|
@@ -277,6 +283,9 @@ function connectH2 (client, socket) {
|
|
|
277
283
|
// Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have
|
|
278
284
|
// work that cannot start. See setNoStreamsTimeout.
|
|
279
285
|
noStreamsTimeout: null,
|
|
286
|
+
// Armed while a PING checks whether the peer is still there after a
|
|
287
|
+
// stream timed out. See probeHttp2Session.
|
|
288
|
+
probeTimeout: null,
|
|
280
289
|
// Sockets start out ref'd. Session ref/unref proxies to the socket, so a
|
|
281
290
|
// single cached flag lets us skip redundant uv ref/unref calls, provided
|
|
282
291
|
// every ref/unref of the session or its socket goes through
|
|
@@ -361,6 +370,11 @@ function connectH2 (client, socket) {
|
|
|
361
370
|
return true
|
|
362
371
|
}
|
|
363
372
|
|
|
373
|
+
// Do not open streams on a connection that may be dead.
|
|
374
|
+
if (session[kHTTP2SessionState].probeTimeout != null) {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
|
|
364
378
|
if (client[kRunning] >= client[kMaxConcurrentStreams]) {
|
|
365
379
|
return true
|
|
366
380
|
}
|
|
@@ -494,6 +508,60 @@ function onNoStreamsTimeout (session) {
|
|
|
494
508
|
resetHttp2Session(session, err)
|
|
495
509
|
}
|
|
496
510
|
|
|
511
|
+
// A stream timeout only resets that stream, unlike HTTP/1.1 which destroys the
|
|
512
|
+
// socket. If the connection itself silently died (dropped by a NAT or load
|
|
513
|
+
// balancer, a hung peer), every new stream would time out in turn and the
|
|
514
|
+
// session would never be replaced. Ask the peer for a PING ack within the
|
|
515
|
+
// timeout that just fired (but at least MIN_PROBE_TIMEOUT): if it answers, only
|
|
516
|
+
// that request was slow; if not, drop the session so queued requests move to a
|
|
517
|
+
// fresh connection.
|
|
518
|
+
function probeHttp2Session (session, timeout) {
|
|
519
|
+
const state = session[kHTTP2SessionState]
|
|
520
|
+
|
|
521
|
+
if (state.probeTimeout != null || session.closed || session.destroyed) {
|
|
522
|
+
return
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
session.ping(onHttp2ProbeAck.bind(session))
|
|
527
|
+
} catch {
|
|
528
|
+
// The session is going away on its own.
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
state.probeTimeout = setTimeout(onHttp2ProbeTimeout, Math.max(timeout, MIN_PROBE_TIMEOUT), session).unref()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function clearHttp2ProbeTimeout (session) {
|
|
536
|
+
const state = session[kHTTP2SessionState]
|
|
537
|
+
|
|
538
|
+
if (state?.probeTimeout != null) {
|
|
539
|
+
clearTimeout(state.probeTimeout)
|
|
540
|
+
state.probeTimeout = null
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function onHttp2ProbeAck () {
|
|
545
|
+
if (this[kHTTP2SessionState].probeTimeout == null) {
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
clearHttp2ProbeTimeout(this)
|
|
550
|
+
this[kClient][kResume]()
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function onHttp2ProbeTimeout (session) {
|
|
554
|
+
session[kHTTP2SessionState].probeTimeout = null
|
|
555
|
+
|
|
556
|
+
if (session.closed || session.destroyed) {
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const err = new SocketError('HTTP/2: connection unresponsive', util.getSocketInfo(session[kSocket]))
|
|
561
|
+
session[kError] = err
|
|
562
|
+
resetHttp2Session(session, err)
|
|
563
|
+
}
|
|
564
|
+
|
|
497
565
|
function clearHttp2IdleTimeout (session) {
|
|
498
566
|
const state = session[kHTTP2SessionState]
|
|
499
567
|
|
|
@@ -686,6 +754,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
|
|
686
754
|
|
|
687
755
|
clearHttp2IdleTimeout(this)
|
|
688
756
|
clearNoStreamsTimeout(this)
|
|
757
|
+
clearHttp2ProbeTimeout(this)
|
|
689
758
|
|
|
690
759
|
if (!this.closed && !this.destroyed) {
|
|
691
760
|
this.close()
|
|
@@ -711,6 +780,7 @@ function onHttp2SessionClose () {
|
|
|
711
780
|
|
|
712
781
|
clearHttp2IdleTimeout(this)
|
|
713
782
|
clearNoStreamsTimeout(this)
|
|
783
|
+
clearHttp2ProbeTimeout(this)
|
|
714
784
|
|
|
715
785
|
if (state.ping.interval != null) {
|
|
716
786
|
clearInterval(state.ping.interval)
|
|
@@ -1610,9 +1680,14 @@ function onTimeout () {
|
|
|
1610
1680
|
// Remove self so timeout doesn't fire again after we handle it
|
|
1611
1681
|
stream.off('timeout', onTimeout)
|
|
1612
1682
|
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1683
|
+
const { responseReceived, session, bodyTimeout, headersTimeout } = state
|
|
1684
|
+
const err = responseReceived
|
|
1685
|
+
? new BodyTimeoutError(`HTTP/2: "stream timeout after ${bodyTimeout}"`)
|
|
1686
|
+
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
|
|
1687
|
+
|
|
1688
|
+
// Probe before aborting: completing the request resumes the queue, which
|
|
1689
|
+
// must not open new streams until the peer has proven it is alive.
|
|
1690
|
+
probeHttp2Session(session, responseReceived ? bodyTimeout : headersTimeout)
|
|
1616
1691
|
state.abort(err)
|
|
1617
1692
|
}
|
|
1618
1693
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { AsyncResource } = require('node:async_hooks')
|
|
3
4
|
const { PoolStats } = require('../util/stats.js')
|
|
4
5
|
const DispatcherBase = require('./dispatcher-base')
|
|
5
6
|
const FixedQueue = require('./fixed-queue')
|
|
@@ -26,6 +27,22 @@ const kRetiring = Symbol('retiring clients')
|
|
|
26
27
|
// Closes every live client, including clients that have been
|
|
27
28
|
// taken out of rotation but are still finishing their own
|
|
28
29
|
// requests.
|
|
30
|
+
// Requests that wait in the Pool queue are dispatched later from whichever
|
|
31
|
+
// callback drains the queue (another request's connect, drain, ...). Capture
|
|
32
|
+
// the caller's async context so Client#dispatch, and the diagnostics channel
|
|
33
|
+
// events it publishes, still run in it.
|
|
34
|
+
class QueuedRequest extends AsyncResource {
|
|
35
|
+
constructor (opts, handler) {
|
|
36
|
+
super('UNDICI_POOL_QUEUED_REQUEST')
|
|
37
|
+
this.opts = opts
|
|
38
|
+
this.handler = handler
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
dispatch (dispatcher) {
|
|
42
|
+
return this.runInAsyncScope(dispatcher.dispatch, dispatcher, this.opts, this.handler)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
function closeClients (pool) {
|
|
30
47
|
const closeAll = []
|
|
31
48
|
for (let i = 0; i < pool[kClients].length; i++) {
|
|
@@ -69,7 +86,7 @@ class PoolBase extends DispatcherBase {
|
|
|
69
86
|
break
|
|
70
87
|
}
|
|
71
88
|
this[kQueued]--
|
|
72
|
-
needDrain = !
|
|
89
|
+
needDrain = !item.dispatch(client)
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
client[kNeedDrain] = needDrain
|
|
@@ -114,7 +131,7 @@ class PoolBase extends DispatcherBase {
|
|
|
114
131
|
const item = queue.shift()
|
|
115
132
|
this[kQueued]--
|
|
116
133
|
|
|
117
|
-
if (!
|
|
134
|
+
if (!item.dispatch(dispatcher)) {
|
|
118
135
|
dispatcher[kNeedDrain] = true
|
|
119
136
|
this[kOnClientBusy](dispatcher)
|
|
120
137
|
hasDispatcher = this[kHasDispatcher]()
|
|
@@ -228,7 +245,7 @@ class PoolBase extends DispatcherBase {
|
|
|
228
245
|
|
|
229
246
|
if (!dispatcher) {
|
|
230
247
|
this[kNeedDrain] = true
|
|
231
|
-
this[kQueue].push(
|
|
248
|
+
this[kQueue].push(new QueuedRequest(opts, handler))
|
|
232
249
|
this[kQueued]++
|
|
233
250
|
} else if (!dispatcher.dispatch(opts, handler)) {
|
|
234
251
|
dispatcher[kNeedDrain] = true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.3",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -122,12 +122,12 @@
|
|
|
122
122
|
"cross-env": "^10.0.0",
|
|
123
123
|
"dns-packet": "^5.4.0",
|
|
124
124
|
"esbuild": "^0.28.0",
|
|
125
|
-
"eslint": "^
|
|
125
|
+
"eslint": "^10.12.0",
|
|
126
126
|
"fast-check": "^4.1.1",
|
|
127
127
|
"husky": "^9.0.7",
|
|
128
128
|
"jest": "^30.0.5",
|
|
129
129
|
"jsondiffpatch": "^0.7.3",
|
|
130
|
-
"neostandard": "^0.
|
|
130
|
+
"neostandard": "^0.14.0",
|
|
131
131
|
"node-forge": "^1.3.1",
|
|
132
132
|
"proxy": "^4.0.0",
|
|
133
133
|
"tsd": "^0.33.0",
|
package/types/api.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import Dispatcher from './dispatcher'
|
|
|
5
5
|
/** Performs an HTTP request. */
|
|
6
6
|
declare function request<TOpaque = null> (
|
|
7
7
|
url: string | URL | UrlObject,
|
|
8
|
-
options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions<TOpaque>, 'origin' | 'path' | 'method'> & Partial<Pick<Dispatcher.RequestOptions, 'method'
|
|
8
|
+
options?: { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions<TOpaque>, 'origin' | 'path' | 'method'> & Partial<Pick<Dispatcher.RequestOptions, 'method'>>
|
|
9
9
|
): Promise<Dispatcher.ResponseData<TOpaque>>
|
|
10
10
|
|
|
11
11
|
/** A faster version of `request`. */
|
package/types/utility.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type AutocompletePrimitiveBaseType<T> =
|
|
2
|
-
T extends string ? string
|
|
3
|
-
T extends number ? number
|
|
4
|
-
T extends boolean ? boolean
|
|
5
|
-
never
|
|
2
|
+
T extends string ? string
|
|
3
|
+
: T extends number ? number
|
|
4
|
+
: T extends boolean ? boolean
|
|
5
|
+
: never
|
|
6
6
|
|
|
7
7
|
export type Autocomplete<T> = T | (AutocompletePrimitiveBaseType<T> & Record<never, never>)
|