undici 7.0.0-alpha.3 → 7.0.0-alpha.5

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.
Files changed (47) hide show
  1. package/README.md +2 -1
  2. package/docs/docs/api/Agent.md +14 -14
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +17 -14
  5. package/docs/docs/api/Client.md +11 -11
  6. package/docs/docs/api/Dispatcher.md +30 -10
  7. package/docs/docs/api/EnvHttpProxyAgent.md +12 -12
  8. package/docs/docs/api/MockAgent.md +3 -3
  9. package/docs/docs/api/MockClient.md +5 -5
  10. package/docs/docs/api/MockPool.md +2 -2
  11. package/docs/docs/api/Pool.md +15 -15
  12. package/docs/docs/api/PoolStats.md +1 -1
  13. package/docs/docs/api/ProxyAgent.md +3 -3
  14. package/docs/docs/api/RetryHandler.md +2 -2
  15. package/docs/docs/api/WebSocket.md +1 -1
  16. package/docs/docs/api/api-lifecycle.md +11 -11
  17. package/docs/docs/best-practices/mocking-request.md +2 -2
  18. package/docs/docs/best-practices/proxy.md +1 -1
  19. package/index.d.ts +1 -1
  20. package/index.js +2 -1
  21. package/lib/api/api-request.js +1 -1
  22. package/lib/cache/memory-cache-store.js +106 -342
  23. package/lib/core/connect.js +5 -0
  24. package/lib/core/request.js +2 -2
  25. package/lib/core/util.js +13 -40
  26. package/lib/dispatcher/client-h2.js +53 -33
  27. package/lib/handler/cache-handler.js +126 -85
  28. package/lib/handler/cache-revalidation-handler.js +45 -13
  29. package/lib/handler/redirect-handler.js +5 -3
  30. package/lib/handler/retry-handler.js +3 -3
  31. package/lib/interceptor/cache.js +213 -92
  32. package/lib/interceptor/dns.js +71 -48
  33. package/lib/util/cache.js +73 -13
  34. package/lib/util/timers.js +19 -1
  35. package/lib/web/cookies/index.js +12 -1
  36. package/lib/web/cookies/parse.js +6 -1
  37. package/lib/web/fetch/body.js +1 -5
  38. package/lib/web/fetch/formdata-parser.js +70 -43
  39. package/lib/web/fetch/headers.js +1 -1
  40. package/lib/web/fetch/index.js +4 -6
  41. package/lib/web/fetch/webidl.js +12 -4
  42. package/package.json +2 -3
  43. package/types/cache-interceptor.d.ts +51 -54
  44. package/types/cookies.d.ts +2 -0
  45. package/types/dispatcher.d.ts +1 -1
  46. package/types/index.d.ts +0 -1
  47. package/types/interceptors.d.ts +0 -1
@@ -144,7 +144,7 @@ function resumeH2 (client) {
144
144
  const socket = client[kSocket]
145
145
 
146
146
  if (socket?.destroyed === false) {
147
- if (client[kSize] === 0 && client[kMaxConcurrentStreams] === 0) {
147
+ if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
148
148
  socket.unref()
149
149
  client[kHTTP2Session].unref()
150
150
  } else {
@@ -184,17 +184,19 @@ function onHttp2SessionEnd () {
184
184
  * @param {number} errorCode
185
185
  */
186
186
  function onHttp2SessionGoAway (errorCode) {
187
- // We cannot recover, so best to close the session and the socket
187
+ // TODO(mcollina): Verify if GOAWAY implements the spec correctly:
188
+ // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
189
+ // Specifically, we do not verify the "valid" stream id.
190
+
188
191
  const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
189
192
  const client = this[kClient]
190
193
 
191
194
  client[kSocket] = null
192
195
  client[kHTTPContext] = null
193
196
 
194
- if (this[kHTTP2Session] !== null) {
195
- this[kHTTP2Session].destroy(err)
196
- this[kHTTP2Session] = null
197
- }
197
+ // this is an HTTP2 session
198
+ this.close()
199
+ this[kHTTP2Session] = null
198
200
 
199
201
  util.destroy(this[kSocket], err)
200
202
 
@@ -218,7 +220,8 @@ function onHttp2SessionClose () {
218
220
 
219
221
  const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
220
222
 
221
- client[kHTTP2Session] = null
223
+ client[kSocket] = null
224
+ client[kHTTPContext] = null
222
225
 
223
226
  if (client.destroyed) {
224
227
  assert(client[kPending] === 0)
@@ -238,6 +241,7 @@ function onHttp2SocketClose () {
238
241
  const client = this[kHTTP2Session][kClient]
239
242
 
240
243
  client[kSocket] = null
244
+ client[kHTTPContext] = null
241
245
 
242
246
  if (this[kHTTP2Session] !== null) {
243
247
  this[kHTTP2Session].destroy(err)
@@ -301,7 +305,7 @@ function writeH2 (client, request) {
301
305
  }
302
306
 
303
307
  /** @type {import('node:http2').ClientHttp2Stream} */
304
- let stream
308
+ let stream = null
305
309
 
306
310
  const { hostname, port } = client[kUrl]
307
311
 
@@ -318,14 +322,21 @@ function writeH2 (client, request) {
318
322
  util.errorRequest(client, request, err)
319
323
 
320
324
  if (stream != null) {
321
- util.destroy(stream, err)
325
+ // Some chunks might still come after abort,
326
+ // let's ignore them
327
+ stream.removeAllListeners('data')
328
+
329
+ // On Abort, we close the stream to send RST_STREAM frame
330
+ stream.close()
331
+
332
+ // We move the running index to the next request
333
+ client[kOnError](err)
334
+ client[kResume]()
322
335
  }
323
336
 
324
337
  // We do not destroy the socket as we can continue using the session
325
338
  // the stream gets destroyed and the session remains to create new streams
326
339
  util.destroy(body, err)
327
- client[kQueue][client[kRunningIdx]++] = null
328
- client[kResume]()
329
340
  }
330
341
 
331
342
  try {
@@ -348,7 +359,7 @@ function writeH2 (client, request) {
348
359
  // We disabled endStream to allow the user to write to the stream
349
360
  stream = session.request(headers, { endStream: false, signal })
350
361
 
351
- if (stream.id && !stream.pending) {
362
+ if (!stream.pending) {
352
363
  request.onUpgrade(null, null, stream)
353
364
  ++session[kOpenStreams]
354
365
  client[kQueue][client[kRunningIdx]++] = null
@@ -438,6 +449,7 @@ function writeH2 (client, request) {
438
449
  endStream: shouldEndStream,
439
450
  signal
440
451
  })
452
+
441
453
  writeBodyH2()
442
454
  }
443
455
 
@@ -454,46 +466,52 @@ function writeH2 (client, request) {
454
466
  // for those scenarios, best effort is to destroy the stream immediately
455
467
  // as there's no value to keep it open.
456
468
  if (request.aborted) {
457
- const err = new RequestAbortedError()
458
- util.errorRequest(client, request, err)
459
- util.destroy(stream, err)
469
+ stream.removeAllListeners('data')
460
470
  return
461
471
  }
462
472
 
463
473
  if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
464
474
  stream.pause()
465
475
  }
476
+ })
466
477
 
467
- stream.on('data', (chunk) => {
468
- if (request.onData(chunk) === false) {
469
- stream.pause()
470
- }
471
- })
478
+ stream.on('data', (chunk) => {
479
+ if (request.onData(chunk) === false) {
480
+ stream.pause()
481
+ }
472
482
  })
473
483
 
474
- stream.once('end', () => {
484
+ stream.once('end', (err) => {
485
+ stream.removeAllListeners('data')
475
486
  // When state is null, it means we haven't consumed body and the stream still do not have
476
487
  // a state.
477
488
  // Present specially when using pipeline or stream
478
489
  if (stream.state?.state == null || stream.state.state < 6) {
479
- request.onComplete([])
480
- }
490
+ // Do not complete the request if it was aborted
491
+ if (!request.aborted) {
492
+ request.onComplete([])
493
+ }
481
494
 
482
- if (session[kOpenStreams] === 0) {
495
+ client[kQueue][client[kRunningIdx]++] = null
496
+ client[kResume]()
497
+ } else {
483
498
  // Stream is closed or half-closed-remote (6), decrement counter and cleanup
484
499
  // It does not have sense to continue working with the stream as we do not
485
500
  // have yet RST_STREAM support on client-side
501
+ --session[kOpenStreams]
502
+ if (session[kOpenStreams] === 0) {
503
+ session.unref()
504
+ }
486
505
 
487
- session.unref()
506
+ abort(err ?? new InformationalError('HTTP/2: stream half-closed (remote)'))
507
+ client[kQueue][client[kRunningIdx]++] = null
508
+ client[kPendingIdx] = client[kRunningIdx]
509
+ client[kResume]()
488
510
  }
489
-
490
- abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
491
- client[kQueue][client[kRunningIdx]++] = null
492
- client[kPendingIdx] = client[kRunningIdx]
493
- client[kResume]()
494
511
  })
495
512
 
496
513
  stream.once('close', () => {
514
+ stream.removeAllListeners('data')
497
515
  session[kOpenStreams] -= 1
498
516
  if (session[kOpenStreams] === 0) {
499
517
  session.unref()
@@ -501,16 +519,18 @@ function writeH2 (client, request) {
501
519
  })
502
520
 
503
521
  stream.once('error', function (err) {
522
+ stream.removeAllListeners('data')
504
523
  abort(err)
505
524
  })
506
525
 
507
526
  stream.once('frameError', (type, code) => {
527
+ stream.removeAllListeners('data')
508
528
  abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
509
529
  })
510
530
 
511
- // stream.on('aborted', () => {
512
- // // TODO(HTTP/2): Support aborted
513
- // })
531
+ stream.on('aborted', () => {
532
+ stream.removeAllListeners('data')
533
+ })
514
534
 
515
535
  // stream.on('timeout', () => {
516
536
  // // TODO(HTTP/2): Support timeout
@@ -4,22 +4,26 @@ const util = require('../core/util')
4
4
  const DecoratorHandler = require('../handler/decorator-handler')
5
5
  const {
6
6
  parseCacheControlHeader,
7
- parseVaryHeader
7
+ parseVaryHeader,
8
+ isEtagUsable
8
9
  } = require('../util/cache')
10
+ const { nowAbsolute } = require('../util/timers.js')
11
+
12
+ function noop () {}
9
13
 
10
14
  /**
11
15
  * Writes a response to a CacheStore and then passes it on to the next handler
12
16
  */
13
17
  class CacheHandler extends DecoratorHandler {
14
18
  /**
15
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
19
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
16
20
  */
17
- #store
21
+ #cacheKey
18
22
 
19
23
  /**
20
- * @type {import('../../types/dispatcher.d.ts').default.RequestOptions}
24
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheStore}
21
25
  */
22
- #requestOptions
26
+ #store
23
27
 
24
28
  /**
25
29
  * @type {import('../../types/dispatcher.d.ts').default.DispatchHandlers}
@@ -27,25 +31,36 @@ class CacheHandler extends DecoratorHandler {
27
31
  #handler
28
32
 
29
33
  /**
30
- * @type {import('../../types/cache-interceptor.d.ts').default.CacheStoreWriteable | undefined}
34
+ * @type {import('node:stream').Writable | undefined}
31
35
  */
32
36
  #writeStream
33
37
 
34
38
  /**
35
39
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions} opts
36
- * @param {import('../../types/dispatcher.d.ts').default.RequestOptions} requestOptions
40
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey
37
41
  * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
38
42
  */
39
- constructor (opts, requestOptions, handler) {
43
+ constructor (opts, cacheKey, handler) {
40
44
  const { store } = opts
41
45
 
42
46
  super(handler)
43
47
 
44
48
  this.#store = store
45
- this.#requestOptions = requestOptions
49
+ this.#cacheKey = cacheKey
46
50
  this.#handler = handler
47
51
  }
48
52
 
53
+ onConnect (abort) {
54
+ if (this.#writeStream) {
55
+ this.#writeStream.destroy()
56
+ this.#writeStream = undefined
57
+ }
58
+
59
+ if (typeof this.#handler.onConnect === 'function') {
60
+ this.#handler.onConnect(abort)
61
+ }
62
+ }
63
+
49
64
  /**
50
65
  * @see {DispatchHandlers.onHeaders}
51
66
  *
@@ -61,72 +76,71 @@ class CacheHandler extends DecoratorHandler {
61
76
  resume,
62
77
  statusMessage
63
78
  ) {
64
- const downstreamOnHeaders = () => this.#handler.onHeaders(
65
- statusCode,
66
- rawHeaders,
67
- resume,
68
- statusMessage
69
- )
79
+ const downstreamOnHeaders = () => {
80
+ if (typeof this.#handler.onHeaders === 'function') {
81
+ return this.#handler.onHeaders(
82
+ statusCode,
83
+ rawHeaders,
84
+ resume,
85
+ statusMessage
86
+ )
87
+ } else {
88
+ return true
89
+ }
90
+ }
70
91
 
71
92
  if (
72
- !util.safeHTTPMethods.includes(this.#requestOptions.method) &&
93
+ !util.safeHTTPMethods.includes(this.#cacheKey.method) &&
73
94
  statusCode >= 200 &&
74
95
  statusCode <= 399
75
96
  ) {
76
- // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-respons
77
- // Try/catch for if it's synchronous
97
+ // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response
78
98
  try {
79
- const result = this.#store.deleteByOrigin(this.#requestOptions.origin)
80
- if (
81
- result &&
82
- typeof result.catch === 'function' &&
83
- typeof this.#handler.onError === 'function'
84
- ) {
85
- // Fail silently
86
- result.catch(_ => {})
87
- }
88
- } catch (err) {
99
+ this.#store.delete(this.#cacheKey).catch?.(noop)
100
+ } catch {
89
101
  // Fail silently
90
102
  }
91
-
92
103
  return downstreamOnHeaders()
93
104
  }
94
105
 
95
- const headers = util.parseHeaders(rawHeaders)
106
+ const parsedRawHeaders = util.parseRawHeaders(rawHeaders)
107
+ const headers = util.parseHeaders(parsedRawHeaders)
96
108
 
97
109
  const cacheControlHeader = headers['cache-control']
98
- const contentLengthHeader = headers['content-length']
110
+ const isCacheFull = typeof this.#store.isFull !== 'undefined'
111
+ ? this.#store.isFull
112
+ : false
99
113
 
100
- if (!cacheControlHeader || !contentLengthHeader || this.#store.isFull) {
101
- // Don't have the headers we need, can't cache
102
- return downstreamOnHeaders()
103
- }
104
-
105
- const contentLength = Number(contentLengthHeader)
106
- if (!Number.isInteger(contentLength)) {
114
+ if (
115
+ !cacheControlHeader ||
116
+ isCacheFull
117
+ ) {
118
+ // Don't have the cache control header or the cache is full
107
119
  return downstreamOnHeaders()
108
120
  }
109
-
110
121
  const cacheControlDirectives = parseCacheControlHeader(cacheControlHeader)
111
122
  if (!canCacheResponse(statusCode, headers, cacheControlDirectives)) {
112
123
  return downstreamOnHeaders()
113
124
  }
114
125
 
115
- const now = Date.now()
126
+ const now = nowAbsolute()
116
127
  const staleAt = determineStaleAt(now, headers, cacheControlDirectives)
117
128
  if (staleAt) {
118
- const varyDirectives = headers.vary
119
- ? parseVaryHeader(headers.vary, this.#requestOptions.headers)
129
+ const varyDirectives = this.#cacheKey.headers && headers.vary
130
+ ? parseVaryHeader(headers.vary, this.#cacheKey.headers)
120
131
  : undefined
121
132
  const deleteAt = determineDeleteAt(now, cacheControlDirectives, staleAt)
122
133
 
123
134
  const strippedHeaders = stripNecessaryHeaders(
124
135
  rawHeaders,
125
- headers,
136
+ parsedRawHeaders,
126
137
  cacheControlDirectives
127
138
  )
128
139
 
129
- this.#writeStream = this.#store.createWriteStream(this.#requestOptions, {
140
+ /**
141
+ * @type {import('../../types/cache-interceptor.d.ts').default.CacheValue}
142
+ */
143
+ const value = {
130
144
  statusCode,
131
145
  statusMessage,
132
146
  rawHeaders: strippedHeaders,
@@ -134,21 +148,33 @@ class CacheHandler extends DecoratorHandler {
134
148
  cachedAt: now,
135
149
  staleAt,
136
150
  deleteAt
137
- })
151
+ }
152
+
153
+ if (typeof headers.etag === 'string' && isEtagUsable(headers.etag)) {
154
+ value.etag = headers.etag
155
+ }
156
+
157
+ this.#writeStream = this.#store.createWriteStream(this.#cacheKey, value)
138
158
 
139
159
  if (this.#writeStream) {
140
- this.#writeStream.on('drain', resume)
141
- this.#writeStream.on('error', () => {
142
- this.#writeStream = undefined
143
- resume()
144
- })
160
+ const handler = this
161
+ this.#writeStream
162
+ .on('drain', resume)
163
+ .on('error', function () {
164
+ // TODO (fix): Make error somehow observable?
165
+ })
166
+ .on('close', function () {
167
+ if (handler.#writeStream === this) {
168
+ handler.#writeStream = undefined
169
+ }
170
+
171
+ // TODO (fix): Should we resume even if was paused downstream?
172
+ resume()
173
+ })
145
174
  }
146
175
  }
147
176
 
148
- if (typeof this.#handler.onHeaders === 'function') {
149
- return downstreamOnHeaders()
150
- }
151
- return false
177
+ return downstreamOnHeaders()
152
178
  }
153
179
 
154
180
  /**
@@ -178,10 +204,6 @@ class CacheHandler extends DecoratorHandler {
178
204
  */
179
205
  onComplete (rawTrailers) {
180
206
  if (this.#writeStream) {
181
- if (rawTrailers) {
182
- this.#writeStream.rawTrailers = rawTrailers
183
- }
184
-
185
207
  this.#writeStream.end()
186
208
  }
187
209
 
@@ -211,7 +233,7 @@ class CacheHandler extends DecoratorHandler {
211
233
  * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
212
234
  *
213
235
  * @param {number} statusCode
214
- * @param {Record<string, string>} headers
236
+ * @param {Record<string, string | string[]>} headers
215
237
  * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
216
238
  */
217
239
  function canCacheResponse (statusCode, headers, cacheControlDirectives) {
@@ -223,7 +245,6 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
223
245
  }
224
246
 
225
247
  if (
226
- !cacheControlDirectives.public ||
227
248
  cacheControlDirectives.private === true ||
228
249
  cacheControlDirectives['no-cache'] === true ||
229
250
  cacheControlDirectives['no-store']
@@ -237,7 +258,11 @@ function canCacheResponse (statusCode, headers, cacheControlDirectives) {
237
258
  }
238
259
 
239
260
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
240
- if (headers['authorization']) {
261
+ if (headers.authorization) {
262
+ if (!cacheControlDirectives.public || typeof headers.authorization !== 'string') {
263
+ return false
264
+ }
265
+
241
266
  if (
242
267
  Array.isArray(cacheControlDirectives['no-cache']) &&
243
268
  cacheControlDirectives['no-cache'].includes('authorization')
@@ -282,9 +307,12 @@ function determineStaleAt (now, headers, cacheControlDirectives) {
282
307
  return now + (maxAge * 1000)
283
308
  }
284
309
 
285
- if (headers.expire) {
310
+ if (headers.expire && typeof headers.expire === 'string') {
286
311
  // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3
287
- return now + (Date.now() - new Date(headers.expire).getTime())
312
+ const expiresDate = new Date(headers.expire)
313
+ if (expiresDate instanceof Date && !isNaN(expiresDate)) {
314
+ return now + (nowAbsolute() - expiresDate.getTime())
315
+ }
288
316
  }
289
317
 
290
318
  return undefined
@@ -306,11 +334,11 @@ function determineDeleteAt (now, cacheControlDirectives, staleAt) {
306
334
  /**
307
335
  * Strips headers required to be removed in cached responses
308
336
  * @param {Buffer[]} rawHeaders
309
- * @param {Record<string, string | string[]>} parsedHeaders
337
+ * @param {string[]} parsedRawHeaders
310
338
  * @param {import('../util/cache.js').CacheControlDirectives} cacheControlDirectives
311
- * @returns {(Buffer|Buffer[])[]}
339
+ * @returns {Buffer[]}
312
340
  */
313
- function stripNecessaryHeaders (rawHeaders, parsedHeaders, cacheControlDirectives) {
341
+ function stripNecessaryHeaders (rawHeaders, parsedRawHeaders, cacheControlDirectives) {
314
342
  const headersToRemove = ['connection']
315
343
 
316
344
  if (Array.isArray(cacheControlDirectives['no-cache'])) {
@@ -321,39 +349,52 @@ function stripNecessaryHeaders (rawHeaders, parsedHeaders, cacheControlDirective
321
349
  headersToRemove.push(...cacheControlDirectives['private'])
322
350
  }
323
351
 
324
- /**
325
- * These are the headers that are okay to cache. If this is assigned, we need
326
- * to remake the buffer representation of the headers
327
- * @type {Record<string, string | string[]> | undefined}
328
- */
329
352
  let strippedHeaders
330
353
 
331
- const headerNames = Object.keys(parsedHeaders)
332
- for (let i = 0; i < headerNames.length; i++) {
333
- const header = headerNames[i]
354
+ let offset = 0
355
+ for (let i = 0; i < parsedRawHeaders.length; i += 2) {
356
+ const headerName = parsedRawHeaders[i]
334
357
 
335
- if (headersToRemove.indexOf(header) !== -1) {
336
- // We have a at least one header we want to remove
358
+ if (headersToRemove.includes(headerName)) {
359
+ // We have at least one header we want to remove
337
360
  if (!strippedHeaders) {
338
- // This is the first header we want to remove, let's create the object
339
- // and backfill the previous headers into it
340
- strippedHeaders = {}
341
-
342
- for (let j = 0; j < i; j++) {
343
- strippedHeaders[headerNames[j]] = parsedHeaders[headerNames[j]]
361
+ // This is the first header we want to remove, let's create the array
362
+ // Since we're stripping headers, this will over allocate. We'll trim
363
+ // it later.
364
+ strippedHeaders = new Array(parsedRawHeaders.length)
365
+
366
+ // Backfill the previous headers into it
367
+ for (let j = 0; j < i; j += 2) {
368
+ strippedHeaders[j] = parsedRawHeaders[j]
369
+ strippedHeaders[j + 1] = parsedRawHeaders[j + 1]
344
370
  }
345
371
  }
346
372
 
373
+ // We can't map indices 1:1 from stripped headers to rawHeaders without
374
+ // creating holes (if we skip a header, we now have two holes where at
375
+ // element should be). So, let's keep an offset to keep strippedHeaders
376
+ // flattened. We can also use this at the end for trimming the empty
377
+ // elements off of strippedHeaders.
378
+ offset += 2
379
+
347
380
  continue
348
381
  }
349
382
 
350
- // This header is fine. Let's add it to strippedHeaders if it exists.
383
+ // We want to keep this header. Let's add it to strippedHeaders if it exists
351
384
  if (strippedHeaders) {
352
- strippedHeaders[header] = parsedHeaders[header]
385
+ strippedHeaders[i - offset] = parsedRawHeaders[i]
386
+ strippedHeaders[i + 1 - offset] = parsedRawHeaders[i + 1]
353
387
  }
354
388
  }
355
389
 
356
- return strippedHeaders ? util.encodeHeaders(strippedHeaders) : rawHeaders
390
+ if (strippedHeaders) {
391
+ // Trim off the empty values at the end
392
+ strippedHeaders.length -= offset
393
+ }
394
+
395
+ return strippedHeaders
396
+ ? util.encodeRawHeaders(strippedHeaders)
397
+ : rawHeaders
357
398
  }
358
399
 
359
400
  module.exports = CacheHandler
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
3
4
  const DecoratorHandler = require('../handler/decorator-handler')
4
5
 
5
6
  /**
@@ -19,29 +20,36 @@ const DecoratorHandler = require('../handler/decorator-handler')
19
20
  class CacheRevalidationHandler extends DecoratorHandler {
20
21
  #successful = false
21
22
  /**
22
- * @type {(() => void)}
23
+ * @type {((boolean) => void) | null}
23
24
  */
24
- #successCallback
25
+ #callback
25
26
  /**
26
27
  * @type {(import('../../types/dispatcher.d.ts').default.DispatchHandlers)}
27
28
  */
28
29
  #handler
29
30
 
31
+ #abort
32
+
30
33
  /**
31
- * @param {() => void} successCallback Function to call if the cached value is valid
34
+ * @param {(boolean) => void} callback Function to call if the cached value is valid
32
35
  * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler
33
36
  */
34
- constructor (successCallback, handler) {
35
- if (typeof successCallback !== 'function') {
36
- throw new TypeError('successCallback must be a function')
37
+ constructor (callback, handler) {
38
+ if (typeof callback !== 'function') {
39
+ throw new TypeError('callback must be a function')
37
40
  }
38
41
 
39
42
  super(handler)
40
43
 
41
- this.#successCallback = successCallback
44
+ this.#callback = callback
42
45
  this.#handler = handler
43
46
  }
44
47
 
48
+ onConnect (abort) {
49
+ this.#successful = false
50
+ this.#abort = abort
51
+ }
52
+
45
53
  /**
46
54
  * @see {DispatchHandlers.onHeaders}
47
55
  *
@@ -57,13 +65,21 @@ class CacheRevalidationHandler extends DecoratorHandler {
57
65
  resume,
58
66
  statusMessage
59
67
  ) {
68
+ assert(this.#callback != null)
69
+
60
70
  // https://www.rfc-editor.org/rfc/rfc9111.html#name-handling-a-validation-respo
61
- if (statusCode === 304) {
62
- this.#successful = true
63
- this.#successCallback()
71
+ this.#successful = statusCode === 304
72
+ this.#callback(this.#successful)
73
+ this.#callback = null
74
+
75
+ if (this.#successful) {
64
76
  return true
65
77
  }
66
78
 
79
+ if (typeof this.#handler.onConnect === 'function') {
80
+ this.#handler.onConnect(this.#abort)
81
+ }
82
+
67
83
  if (typeof this.#handler.onHeaders === 'function') {
68
84
  return this.#handler.onHeaders(
69
85
  statusCode,
@@ -72,7 +88,8 @@ class CacheRevalidationHandler extends DecoratorHandler {
72
88
  statusMessage
73
89
  )
74
90
  }
75
- return false
91
+
92
+ return true
76
93
  }
77
94
 
78
95
  /**
@@ -90,7 +107,7 @@ class CacheRevalidationHandler extends DecoratorHandler {
90
107
  return this.#handler.onData(chunk)
91
108
  }
92
109
 
93
- return false
110
+ return true
94
111
  }
95
112
 
96
113
  /**
@@ -99,7 +116,11 @@ class CacheRevalidationHandler extends DecoratorHandler {
99
116
  * @param {string[] | null} rawTrailers
100
117
  */
101
118
  onComplete (rawTrailers) {
102
- if (!this.#successful && typeof this.#handler.onComplete === 'function') {
119
+ if (this.#successful) {
120
+ return
121
+ }
122
+
123
+ if (typeof this.#handler.onComplete === 'function') {
103
124
  this.#handler.onComplete(rawTrailers)
104
125
  }
105
126
  }
@@ -110,8 +131,19 @@ class CacheRevalidationHandler extends DecoratorHandler {
110
131
  * @param {Error} err
111
132
  */
112
133
  onError (err) {
134
+ if (this.#successful) {
135
+ return
136
+ }
137
+
138
+ if (this.#callback) {
139
+ this.#callback(false)
140
+ this.#callback = null
141
+ }
142
+
113
143
  if (typeof this.#handler.onError === 'function') {
114
144
  this.#handler.onError(err)
145
+ } else {
146
+ throw err
115
147
  }
116
148
  }
117
149
  }