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.
@@ -341,8 +341,21 @@ backing store.
341
341
 
342
342
  **Parameters**
343
343
 
344
- * `opts` {CacheHandler.CacheOptions} (optional) See the
345
- [`CacheStore`](./CacheStore.md) documentation for accepted fields.
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 err = state.responseReceived
1614
- ? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`)
1615
- : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${state.headersTimeout}"`)
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 = !client.dispatch(item.opts, item.handler)
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 (!dispatcher.dispatch(item.opts, item.handler)) {
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({ opts, handler })
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.2",
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": "^9.9.0",
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.13.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`. */
@@ -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>)