undici 8.12.0 → 8.12.2

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.
@@ -148,8 +148,13 @@ added: v1.0.0
148
148
  payloads, and decompressed (`permessage-deflate`) messages. Set to `0` to
149
149
  disable the limit. **Default:** `134217728`.
150
150
  * `eventSource` {Object} (optional) EventSource-specific configuration.
151
- * `maxEventSize` {number} The maximum allowed event size, in bytes, for
152
- EventSource messages. Set to `0` to disable the limit.
151
+ * `maxEventSize` {number} The maximum total bytes of data and metadata
152
+ field values retained for a pending EventSource event. Each unfinished
153
+ line is separately limited to this value plus seven syntax bytes, before
154
+ its line ending arrives. The number of `data:` lines per event and input
155
+ chunks buffered for an unfinished line are each limited to
156
+ `min(131072, max(1024, floor(maxEventSize / 128)))` to bound object overhead.
157
+ Set to `0` to disable these limits.
153
158
  **Default:** `buffer.kStringMaxLength`.
154
159
  * Returns: {Client}
155
160
 
@@ -6,6 +6,7 @@ module.exports = {
6
6
  kDispatch: Symbol('dispatch'),
7
7
  kUrl: Symbol('url'),
8
8
  kRequestOrigin: Symbol('request origin'),
9
+ kRedirectCacheControl: Symbol('redirect cache control'),
9
10
  kOriginless: Symbol('originless'),
10
11
  kWriting: Symbol('writing'),
11
12
  kResuming: Symbol('resuming'),
@@ -22,7 +22,7 @@ function defaultFactory (origin, opts) {
22
22
  }
23
23
 
24
24
  class Agent extends DispatcherBase {
25
- constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, ...options } = {}) {
25
+ constructor ({ factory = defaultFactory, maxOrigins = Infinity, connect, tls, ...options } = {}) {
26
26
  if (typeof factory !== 'function') {
27
27
  throw new InvalidArgumentError('factory must be a function.')
28
28
  }
@@ -41,7 +41,11 @@ class Agent extends DispatcherBase {
41
41
  connect = { ...connect }
42
42
  }
43
43
 
44
- this[kOptions] = { ...util.deepClone(options), maxOrigins, connect }
44
+ if (tls && typeof tls !== 'function') {
45
+ tls = { ...tls }
46
+ }
47
+
48
+ this[kOptions] = { ...util.deepClone(options), maxOrigins, connect, tls }
45
49
  this[kFactory] = factory
46
50
  this[kClients] = new Map()
47
51
  this[kOrigins] = new Set()
@@ -988,8 +988,6 @@ function connectH1 (client, socket) {
988
988
  }
989
989
 
990
990
  function onHttpSocketError (err) {
991
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
992
-
993
991
  const parser = this[kParser]
994
992
 
995
993
  // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
@@ -600,8 +600,6 @@ function onHttp2SendPing (session) {
600
600
  }
601
601
 
602
602
  function onHttp2SessionError (err) {
603
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
604
-
605
603
  this[kSocket][kError] = err
606
604
 
607
605
  if (this[kReceivedGoAway]) {
@@ -767,8 +765,6 @@ function onHttp2SocketClose () {
767
765
  }
768
766
 
769
767
  function onHttp2SocketError (err) {
770
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
771
-
772
768
  this[kError] = err
773
769
 
774
770
  if (this[kHTTP2Session]?.[kReceivedGoAway]) {
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const util = require('../core/util')
4
+ const { kRedirectCacheControl } = require('../core/symbols')
4
5
  const {
5
6
  parseCacheControlHeader,
6
7
  hasInvalidCacheControlDirective,
@@ -57,6 +58,19 @@ class CacheController {
57
58
  set rawTrailers (value) { this.target.rawTrailers = value }
58
59
  }
59
60
 
61
+ /**
62
+ * @param {string | URL} a
63
+ * @param {string | URL} b
64
+ * @returns {boolean}
65
+ */
66
+ function sameOrigin (a, b) {
67
+ try {
68
+ return new URL(String(a)).origin === new URL(String(b)).origin
69
+ } catch {
70
+ return String(a) === String(b)
71
+ }
72
+ }
73
+
60
74
  // Status codes that we can use some heuristics on to cache
61
75
  const HEURISTICALLY_CACHEABLE_STATUS_CODES = [
62
76
  200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501
@@ -203,6 +217,14 @@ class CacheHandler {
203
217
  */
204
218
  #writeStream
205
219
 
220
+ /**
221
+ * The URLs visited while following redirects, shared by reference from the
222
+ * redirect handler through the request context. Carries the suppressed hops'
223
+ * cache-control under {@link kRedirectCacheControl}.
224
+ * @type {URL[] | undefined}
225
+ */
226
+ #history
227
+
206
228
  /**
207
229
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheHandlerOptions} opts
208
230
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
@@ -220,6 +242,13 @@ class CacheHandler {
220
242
  this.#writeStream?.destroy()
221
243
  this.#writeStream = undefined
222
244
  this.#downstreamController = new CacheController(controller)
245
+ // The redirect handler threads the list of visited URLs through the
246
+ // handler-chain context. Keep a reference to it (it is mutated as redirects
247
+ // are followed) so onResponseStart can tell which request actually produced
248
+ // the response. Unlike the request options object, this reference is not
249
+ // broken by an interceptor that copies the options between the cache and
250
+ // the redirect.
251
+ this.#history = context?.history
223
252
  this.#handler.onRequestStart?.(this.#downstreamController, context)
224
253
  }
225
254
 
@@ -267,6 +296,35 @@ class CacheHandler {
267
296
  return downstreamOnHeaders()
268
297
  }
269
298
 
299
+ // A cross-origin redirect can change the origin that actually produced the
300
+ // final response. Keying that response on the original (trusted) origin
301
+ // would let a redirect target control an entry for a URL the application
302
+ // trusts. The final request is the last URL in the redirect history; only
303
+ // cache when its origin still matches the cache key origin.
304
+ if (this.#history && this.#history.length > 0) {
305
+ const finalUrl = this.#history[this.#history.length - 1]
306
+ if (!sameOrigin(finalUrl.origin, this.#cacheKey.origin)) {
307
+ return downstreamOnHeaders()
308
+ }
309
+ }
310
+
311
+ // A redirect hop that is not forwarded can still carry a restrictive
312
+ // storage policy (for example `no-store`, or `private` for a shared cache).
313
+ // Honor it, otherwise a redirect that must not be stored could be laundered
314
+ // into a cacheable final response under the original key.
315
+ const redirectCacheControl = this.#history?.[kRedirectCacheControl]
316
+ if (redirectCacheControl) {
317
+ for (const hopCacheControl of redirectCacheControl) {
318
+ const hopDirectives = hopCacheControl ? parseCacheControlHeader(hopCacheControl) : {}
319
+ if (
320
+ hopDirectives['no-store'] === true ||
321
+ (this.#cacheType === 'shared' && hopDirectives.private === true)
322
+ ) {
323
+ return downstreamOnHeaders()
324
+ }
325
+ }
326
+ }
327
+
270
328
  const cacheControlHeader = resHeaders['cache-control']
271
329
  const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
272
330
 
@@ -16,6 +16,7 @@ const DEFAULT_MAX_BUFFER_SIZE = 5 * 1024 * 1024
16
16
  * @property {number} bufferedBytes
17
17
  * @property {object | null} pendingTrailers
18
18
  * @property {boolean} done
19
+ * @property {boolean} [pendingStart] onRequestStart deferred until the primary request's context is known
19
20
  */
20
21
 
21
22
  /**
@@ -30,6 +31,20 @@ class DeduplicationHandler {
30
31
  */
31
32
  #primaryHandler
32
33
 
34
+ /**
35
+ * Whether the primary request has started (onRequestStart received). Until
36
+ * then the request context is unknown and cannot be given to waiting handlers.
37
+ * @type {boolean}
38
+ */
39
+ #requestStarted = false
40
+
41
+ /**
42
+ * The primary request context, replayed to waiting handlers so they observe
43
+ * the same redirect history as the primary.
44
+ * @type {any}
45
+ */
46
+ #context = null
47
+
33
48
  /**
34
49
  * @type {WaitingHandler[]}
35
50
  */
@@ -112,20 +127,30 @@ class DeduplicationHandler {
112
127
  const waitingController = waitingHandler.controller
113
128
 
114
129
  try {
115
- handler.onRequestStart?.(waitingController, null)
130
+ if (this.#requestStarted) {
131
+ // The primary request has started, so its context (including the
132
+ // redirect history) is known and can be delivered immediately.
133
+ handler.onRequestStart?.(waitingController, this.#context)
116
134
 
117
- if (waitingController.aborted) {
118
- waitingHandler.done = true
119
- return true
120
- }
135
+ if (waitingController.aborted) {
136
+ waitingHandler.done = true
137
+ return true
138
+ }
121
139
 
122
- if (this.#responseStarted) {
123
- handler.onResponseStart?.(
124
- waitingController,
125
- this.#statusCode,
126
- this.#headers,
127
- this.#statusMessage
128
- )
140
+ if (this.#responseStarted) {
141
+ handler.onResponseStart?.(
142
+ waitingController,
143
+ this.#statusCode,
144
+ this.#headers,
145
+ this.#statusMessage
146
+ )
147
+ }
148
+ } else {
149
+ // The primary request has not started yet. Defer onRequestStart until
150
+ // its context is known, so the waiting handler observes the same
151
+ // redirect history as the primary (a cache handler relies on it to
152
+ // reject cross-origin redirect responses).
153
+ waitingHandler.pendingStart = true
129
154
  }
130
155
  } catch {
131
156
  // Ignore errors from waiting handlers
@@ -146,6 +171,24 @@ class DeduplicationHandler {
146
171
  */
147
172
  onRequestStart (controller, context) {
148
173
  this.#controller = controller
174
+ if (!this.#requestStarted) {
175
+ this.#requestStarted = true
176
+ this.#context = context
177
+ // Deliver the now-known context to handlers that joined before the
178
+ // primary request started.
179
+ for (const waitingHandler of this.#waitingHandlers) {
180
+ if (waitingHandler.pendingStart && !waitingHandler.done && !waitingHandler.controller.aborted) {
181
+ waitingHandler.pendingStart = false
182
+ try {
183
+ waitingHandler.handler.onRequestStart?.(waitingHandler.controller, context)
184
+ } catch {
185
+ // Ignore errors from waiting handlers
186
+ waitingHandler.done = true
187
+ }
188
+ }
189
+ }
190
+ this.#pruneDoneWaitingHandlers()
191
+ }
149
192
  this.#primaryHandler.onRequestStart?.(controller, context)
150
193
  }
151
194
 
@@ -330,7 +373,8 @@ class DeduplicationHandler {
330
373
  bufferedChunks: [],
331
374
  bufferedBytes: 0,
332
375
  pendingTrailers: null,
333
- done: false
376
+ done: false,
377
+ pendingStart: false
334
378
  }
335
379
 
336
380
  const state = {
@@ -3,7 +3,7 @@
3
3
  const util = require('../core/util')
4
4
  const assert = require('node:assert')
5
5
  const { InvalidArgumentError, MaxRedirectsError } = require('../core/errors')
6
- const { kRequestOrigin } = require('../core/symbols')
6
+ const { kRequestOrigin, kRedirectCacheControl } = require('../core/symbols')
7
7
 
8
8
  const redirectableStatusCodes = [300, 301, 302, 303, 307, 308]
9
9
 
@@ -38,6 +38,15 @@ class RedirectHandler {
38
38
  this.maxRedirections = maxRedirections
39
39
  this.handler = handler
40
40
  this.history = []
41
+ // `cache-control` of each intermediate (suppressed) redirect hop. An outer
42
+ // cache uses it to preserve a hop's storage policy (e.g. `no-store`) that
43
+ // would otherwise be lost because the hop's response is not forwarded. It
44
+ // travels as a non-enumerable property of the history array so it reaches
45
+ // the cache without becoming part of the public request/response context.
46
+ Object.defineProperty(this.history, kRedirectCacheControl, {
47
+ value: [],
48
+ enumerable: false
49
+ })
41
50
  }
42
51
 
43
52
  onRequestStart (controller, context) {
@@ -114,6 +123,11 @@ class RedirectHandler {
114
123
  return
115
124
  }
116
125
 
126
+ // This hop's response is suppressed (only its Location is used). Record its
127
+ // cache-control so an outer cache can honor a restrictive policy that would
128
+ // otherwise be lost when the final response is stored.
129
+ this.history[kRedirectCacheControl].push(headers['cache-control'])
130
+
117
131
  const baseUrl = requestOrigin
118
132
  ? new URL(this.opts.path, requestOrigin)
119
133
  : undefined
@@ -223,9 +223,14 @@ class DecompressHandler extends DecoratorHandler {
223
223
  this.#decompressedSize = decompressedSize
224
224
  }
225
225
 
226
- const result = super.onResponseData(this.#controller, chunk)
227
- if (result === false && !this.#controller.paused) {
228
- this.#controller.pause()
226
+ try {
227
+ const result = super.onResponseData(this.#controller, chunk)
228
+ if (result === false && !this.#controller.paused) {
229
+ this.#controller.pause()
230
+ }
231
+ } catch (err) {
232
+ this.#fail(err)
233
+ return
229
234
  }
230
235
  }
231
236
  } while (this.#drainRequested && !this.#terminated && !this.#controller.paused)
@@ -242,9 +247,14 @@ class DecompressHandler extends DecoratorHandler {
242
247
  return
243
248
  }
244
249
 
245
- this.#terminated = true
246
250
  this.#cleanupDecompressors()
247
- super.onResponseEnd(this.#controller, this.#trailers)
251
+ try {
252
+ super.onResponseEnd(this.#controller, this.#trailers)
253
+ } catch (err) {
254
+ this.#fail(err)
255
+ return
256
+ }
257
+ this.#terminated = true
248
258
  }
249
259
 
250
260
  #onDecompressionEnd () {
@@ -31,12 +31,11 @@ const EVENT = Buffer.from('event')
31
31
  const ID = Buffer.from('id')
32
32
  const RETRY = Buffer.from('retry')
33
33
 
34
- // The longest prefix a `data:` field can have before its value: "data:" plus a
35
- // single leading U+0020 SPACE. Used when bounding an unterminated line against
36
- // maxEventSize so a server cannot force unbounded buffering by never sending a
37
- // line terminator, while still accepting a terminated data line whose value is
38
- // exactly maxEventSize bytes.
39
- const DATA_FIELD_PREFIX_SIZE = DATA.length + 2
34
+ // Each data field creates a string node, and each unconsumed input chunk keeps
35
+ // a Buffer wrapper. The absolute ceiling protects the large default byte limit.
36
+ const minParserItems = 1024
37
+ const maxParserItems = 131072
38
+ const bytesPerParserItem = 128
40
39
 
41
40
  function isASCIINumberBytes (buffer, start) {
42
41
  if (start >= buffer.length) {
@@ -130,9 +129,13 @@ class EventSourceStream extends Transform {
130
129
  pos = 0
131
130
  lineChunkIndex = 0
132
131
  linePos = 0
133
- eventDataSize = 0
134
- currentLineSize = 0
132
+ lineLength = 0
133
+ eventSize = 0
134
+ eventFieldSizes = { data: 0, event: 0, id: 0, retry: 0 }
135
+ dataLineCount = 0
135
136
  maxEventSize
137
+ maxDataLines
138
+ maxBufferedChunks
136
139
 
137
140
  event = {
138
141
  data: undefined,
@@ -157,6 +160,11 @@ class EventSourceStream extends Transform {
157
160
 
158
161
  this.state = options.eventSourceSettings || {}
159
162
  this.maxEventSize = options.maxEventSize ?? defaultMaxEventSize
163
+ const parserItemLimit = this.maxEventSize > 0
164
+ ? Math.min(maxParserItems, Math.max(minParserItems, Math.floor(this.maxEventSize / bytesPerParserItem)))
165
+ : 0
166
+ this.maxDataLines = parserItemLimit
167
+ this.maxBufferedChunks = parserItemLimit
160
168
  if (options.push) {
161
169
  this.push = options.push
162
170
  }
@@ -174,6 +182,14 @@ class EventSourceStream extends Transform {
174
182
  return
175
183
  }
176
184
 
185
+ // A leading line ending can immediately release the currently buffered
186
+ // line, even when it uses the final allowed chunk slot.
187
+ if (this.maxEventSize > 0 &&
188
+ this.chunks.length >= this.maxBufferedChunks &&
189
+ chunk[0] !== LF && chunk[0] !== CR) {
190
+ callback(createMaxEventSizeExceededError())
191
+ return
192
+ }
177
193
  this.chunks.push(chunk)
178
194
 
179
195
  // Strip leading byte-order-mark if we opened the stream and started
@@ -266,21 +282,25 @@ class EventSourceStream extends Transform {
266
282
  continue
267
283
  }
268
284
 
269
- this.advanceCursor()
270
- this.currentLineSize++
271
-
272
- // An unterminated line never reaches parseLine(), so enforce the configured
273
- // maxEventSize here to prevent unbounded buffering when a server streams a
274
- // field that never contains a line terminator.
275
- if (this.currentLineSize > this.getMaxEventSizeLineLength()) {
285
+ // readLine() is only called after CR or LF. Reject an unfinished line
286
+ // before it can retain an unbounded number of decoded chunks. The seven
287
+ // extra bytes allow the longest field prefix ("event: " or "retry: ").
288
+ if (this.maxEventSize > 0 && ++this.lineLength > this.maxEventSize + 7) {
276
289
  callback(createMaxEventSizeExceededError())
277
290
  return
278
291
  }
292
+ this.advanceCursor()
279
293
  }
280
294
 
281
295
  callback()
282
296
  }
283
297
 
298
+ _destroy (error, callback) {
299
+ this.chunks.length = 0
300
+ this.clearEvent()
301
+ super._destroy(error, callback)
302
+ }
303
+
284
304
  /**
285
305
  * @param {Buffer} line
286
306
  * @param {EventSourceStreamEvent} event
@@ -317,12 +337,12 @@ class EventSourceStream extends Transform {
317
337
  }
318
338
 
319
339
  if (isFieldName(line, fieldLength, DATA)) {
320
- const valueBytes = line.length - valueStart
321
- const eventDataSize = this.eventDataSize + (event.data === undefined ? 0 : 1) + valueBytes
322
-
323
- if (this.maxEventSize > 0 && eventDataSize > this.maxEventSize) {
340
+ if (this.maxEventSize > 0 && this.dataLineCount >= this.maxDataLines) {
324
341
  throw createMaxEventSizeExceededError()
325
342
  }
343
+ const valueBytes = line.length - valueStart
344
+ const dataSize = this.eventFieldSizes.data + (event.data === undefined ? 0 : 1) + valueBytes
345
+ this.setEventFieldSize('data', dataSize)
326
346
 
327
347
  const value = line.toString('utf8', valueStart)
328
348
 
@@ -331,12 +351,13 @@ class EventSourceStream extends Transform {
331
351
  } else {
332
352
  event.data += `\n${value}`
333
353
  }
334
- this.eventDataSize = eventDataSize
354
+ this.dataLineCount++
335
355
  return
336
356
  }
337
357
 
338
358
  if (isFieldName(line, fieldLength, RETRY)) {
339
359
  if (isASCIINumberBytes(line, valueStart)) {
360
+ this.setEventFieldSize('retry', line.length - valueStart)
340
361
  event.retry = line.toString('utf8', valueStart)
341
362
  }
342
363
  return
@@ -344,20 +365,29 @@ class EventSourceStream extends Transform {
344
365
 
345
366
  if (isFieldName(line, fieldLength, ID)) {
346
367
  if (isValidLastEventIdBytes(line, valueStart)) {
368
+ this.setEventFieldSize('id', line.length - valueStart)
347
369
  event.id = line.toString('utf8', valueStart)
348
370
  }
349
371
  return
350
372
  }
351
373
 
352
374
  if (isFieldName(line, fieldLength, EVENT)) {
353
- const value = line.toString('utf8', valueStart)
354
-
355
- if (value.length > 0) {
356
- event.event = value
375
+ if (valueStart < line.length) {
376
+ this.setEventFieldSize('event', line.length - valueStart)
377
+ event.event = line.toString('utf8', valueStart)
357
378
  }
358
379
  }
359
380
  }
360
381
 
382
+ setEventFieldSize (field, size) {
383
+ const eventSize = this.eventSize - this.eventFieldSizes[field] + size
384
+ if (this.maxEventSize > 0 && eventSize > this.maxEventSize) {
385
+ throw createMaxEventSizeExceededError()
386
+ }
387
+ this.eventSize = eventSize
388
+ this.eventFieldSizes[field] = size
389
+ }
390
+
361
391
  /**
362
392
  * @param {EventSourceStreamEvent} event
363
393
  */
@@ -388,7 +418,12 @@ class EventSourceStream extends Transform {
388
418
  this.event.event = undefined
389
419
  this.event.id = undefined
390
420
  this.event.retry = undefined
391
- this.eventDataSize = 0
421
+ this.eventSize = 0
422
+ this.dataLineCount = 0
423
+ this.eventFieldSizes.data = 0
424
+ this.eventFieldSizes.event = 0
425
+ this.eventFieldSizes.id = 0
426
+ this.eventFieldSizes.retry = 0
392
427
  }
393
428
 
394
429
  hasPendingEvent () {
@@ -424,29 +459,16 @@ class EventSourceStream extends Transform {
424
459
  syncLineStartToCursor () {
425
460
  this.lineChunkIndex = this.chunkIndex
426
461
  this.linePos = this.pos
427
- this.currentLineSize = 0
462
+ this.lineLength = 0
428
463
  this.dropConsumedChunks()
429
464
  }
430
465
 
431
- /**
432
- * @returns {number} The maximum length, in bytes, of the currently buffered
433
- * (possibly unterminated) line. The extra {@link DATA_FIELD_PREFIX_SIZE}
434
- * accounts for the longest `data:` field prefix, so a terminated data line
435
- * carrying exactly {@link maxEventSize} value bytes remains accepted.
436
- */
437
- getMaxEventSizeLineLength () {
438
- if (this.maxEventSize <= 0) {
439
- return Infinity
440
- }
441
-
442
- return this.maxEventSize + DATA_FIELD_PREFIX_SIZE
443
- }
444
-
445
466
  dropConsumedChunks () {
446
- while (this.lineChunkIndex > 0) {
447
- this.chunks.shift()
448
- this.lineChunkIndex--
449
- this.chunkIndex--
467
+ if (this.lineChunkIndex > 0) {
468
+ const consumed = this.lineChunkIndex
469
+ this.chunks.splice(0, consumed)
470
+ this.lineChunkIndex = 0
471
+ this.chunkIndex -= consumed
450
472
  }
451
473
 
452
474
  if (this.chunkIndex === this.chunks.length) {
@@ -2280,8 +2280,6 @@ async function httpNetworkFetch (
2280
2280
  appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)
2281
2281
  const location = headersList.get('location', true)
2282
2282
 
2283
- this.body = new Readable({ read: () => controller.resume() })
2284
-
2285
2283
  const willFollow = location && request.redirect === 'follow' &&
2286
2284
  redirectStatusSet.has(status)
2287
2285
 
@@ -2299,7 +2297,7 @@ async function httpNetworkFetch (
2299
2297
  // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
2300
2298
  const maxContentEncodings = 5
2301
2299
  if (codings.length > maxContentEncodings) {
2302
- reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
2300
+ controller.abort(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
2303
2301
  return
2304
2302
  }
2305
2303
 
@@ -2341,6 +2339,8 @@ async function httpNetworkFetch (
2341
2339
  }
2342
2340
  }
2343
2341
 
2342
+ this.body = new Readable({ read: () => controller.resume() })
2343
+
2344
2344
  const onError = (err) => this.onResponseError(controller, err)
2345
2345
 
2346
2346
  resolve({
@@ -2,6 +2,7 @@
2
2
 
3
3
  const assert = require('node:assert')
4
4
  const { runtimeFeatures } = require('../../util/runtime-features.js')
5
+ const { asciiLowercase } = require('../infra/index.js')
5
6
 
6
7
  /**
7
8
  * @typedef {object} Metadata
@@ -66,14 +67,12 @@ const getSRIHashAlgorithmIndex = /** @type {GetSRIHashAlgorithmIndex} */ (Map.pr
66
67
 
67
68
  /**
68
69
  * @typedef IsValidSRIHashAlgorithm
70
+ * @see https://w3c.github.io/webappsec-subresource-integrity/#valid-sri-hash-algorithm-token
69
71
  * @type {(algorithm: string) => algorithm is SRIHashAlgorithm}
70
72
  * @param {*} algorithm
71
73
  * @returns {algorithm is SRIHashAlgorithm}
72
74
  */
73
-
74
- const isValidSRIHashAlgorithm = /** @type {IsValidSRIHashAlgorithm} */ (
75
- Map.prototype.has.bind(validSRIHashAlgorithmTokenSet)
76
- )
75
+ const isValidSRIHashAlgorithm = (algorithm) => validSRIHashAlgorithmTokenSet.has(asciiLowercase(algorithm))
77
76
 
78
77
  /**
79
78
  * @param {Uint8Array} bytes
@@ -229,7 +228,8 @@ function parseMetadata (metadata) {
229
228
  // 8. Let metadata be the ordered map
230
229
  // «["alg" → algorithm, "val" → base64-value]».
231
230
  const metadata = {
232
- alg: algorithm,
231
+ // Possible spec bug?
232
+ alg: asciiLowercase(algorithm),
233
233
  val: base64Value
234
234
  }
235
235
 
@@ -252,6 +252,19 @@ class ByteParser extends Writable {
252
252
  if (isControlFrame(this.#info.opcode)) {
253
253
  this.#loop = this.parseControlFrame(body)
254
254
  this.#state = parserStates.INFO
255
+
256
+ // If the automatic PONG/CLOSE reply filled the socket's write buffer,
257
+ // stop parsing and resume on drain so a PING flood cannot grow the
258
+ // outbound queue without bound.
259
+ if (this.#loop && this.#handler.socket.writableNeedDrain) {
260
+ this.#loop = false
261
+ this.#handler.socket.once('drain', () => {
262
+ if (this.destroyed) return
263
+ this.#loop = true
264
+ this.run(callback)
265
+ })
266
+ return
267
+ }
255
268
  } else {
256
269
  if (!this.#info.compressed) {
257
270
  if (!this.writeFragments(body)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "8.12.0",
3
+ "version": "8.12.2",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -65,6 +65,7 @@
65
65
  "build:node": "esbuild index-fetch.js --bundle --platform=node --outfile=undici-fetch.js --define:esbuildDetection=1 --keep-names && node scripts/strip-comments.js",
66
66
  "build:milo": "node build/milo.js",
67
67
  "build:llhttp": "node build/llhttp.js --docker",
68
+ "build:wasm": "node build/llhttp.js --docker",
68
69
  "generate-pem": "node scripts/generate-pem.js",
69
70
  "lint": "eslint --cache",
70
71
  "lint:fix": "eslint --fix --cache",
package/types/client.d.ts CHANGED
@@ -175,8 +175,12 @@ export declare namespace Client {
175
175
  }
176
176
  export interface EventSourceOptions {
177
177
  /**
178
- * Maximum allowed event size in bytes for EventSource messages.
179
- * Set to 0 to disable the limit.
178
+ * Maximum total bytes of data and metadata field values retained for a
179
+ * pending EventSource event. Each unfinished line is separately bounded
180
+ * to this value plus seven syntax bytes. The number of data lines per
181
+ * event and buffered input chunks are each limited to
182
+ * min(131072, max(1024, floor(maxEventSize / 128))).
183
+ * Set to 0 to disable these limits.
180
184
  * @default buffer.kStringMaxLength
181
185
  */
182
186
  maxEventSize?: number;