undici 7.0.0-alpha.6 → 7.0.0-alpha.8

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.
@@ -13,9 +13,21 @@ The `MemoryCacheStore` stores the responses in-memory.
13
13
 
14
14
  **Options**
15
15
 
16
- - `maxEntries` - The maximum amount of responses to store. Default `Infinity`.
16
+ - `maxCount` - The maximum amount of responses to store. Default `Infinity`.
17
17
  - `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached.
18
18
 
19
+ ### `SqliteCacheStore`
20
+
21
+ The `SqliteCacheStore` stores the responses in a SQLite database.
22
+ Under the hood, it uses Node.js' [`node:sqlite`](https://nodejs.org/api/sqlite.html) api.
23
+ The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present.
24
+
25
+ **Options**
26
+
27
+ - `location` - The location of the SQLite database to use. Default `:memory:`.
28
+ - `maxCount` - The maximum number of entries to store in the database. Default `Infinity`.
29
+ - `maxEntrySize` - The maximum size in bytes that a resposne's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
30
+
19
31
  ## Defining a Custom Cache Store
20
32
 
21
33
  The store must implement the following functions:
@@ -205,14 +205,12 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
205
205
 
206
206
  #### Parameter: `DispatchHandler`
207
207
 
208
- * **onConnect** `(abort: () => void, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
209
- * **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
210
- * **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
211
- * **onResponseStarted** `() => void` (optional) - Invoked when response is received, before headers have been read.
212
- * **onHeaders** `(statusCode: number, headers: Buffer[], resume: () => void, statusText: string) => boolean` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
213
- * **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
214
- * **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
215
- * **onBodySent** `(chunk: string | Buffer | Uint8Array) => void` - Invoked when a body chunk is sent to the server. Not required. For a stream or iterable body this will be invoked for every chunk. For other body types, it will be invoked once after the body is sent.
208
+ * **onRequestStart** `(controller: DispatchController, context: object) => void` - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.
209
+ * **onRequestUpgrade** `(controller: DispatchController, statusCode: number, headers: Record<string, string | string[]>, socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
210
+ * **onResponseStart** `(controller: DispatchController, statusCode: number, statusMessage?: string, headers: Record<string, string | string []>) => void` - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for `upgrade` requests.
211
+ * **onResponseData** `(controller: DispatchController, chunk: Buffer) => void` - Invoked when response payload data is received. Not required for `upgrade` requests.
212
+ * **onResponseEnd** `(controller: DispatchController, trailers: Record<string, string | string[]>) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
213
+ * **onResponseError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
216
214
 
217
215
  #### Example 1 - Dispatch GET request
218
216
 
@@ -1262,6 +1260,8 @@ The `cache` interceptor implements client-side response caching as described in
1262
1260
 
1263
1261
  - `store` - The [`CacheStore`](/docs/docs/api/CacheStore.md) to store and retrieve responses from. Default is [`MemoryCacheStore`](/docs/docs/api/CacheStore.md#memorycachestore).
1264
1262
  - `methods` - The [**safe** HTTP methods](https://www.rfc-editor.org/rfc/rfc9110#section-9.2.1) to cache the response of.
1263
+ - `cacheByDefault` - The default expiration time to cache responses by if they don't have an explicit expiration. If this isn't present, responses without explicit expiration will not be cached. Default `undefined`.
1264
+ - `type` - The type of cache for Undici to act as. Can be `shared` or `private`. Default `shared`.
1265
1265
 
1266
1266
  ## Instance Events
1267
1267
 
@@ -16,7 +16,7 @@ Returns: `RedirectHandler`
16
16
 
17
17
  ### Parameters
18
18
 
19
- - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection.
19
+ - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection.
20
20
  - **maxRedirections** `number` (required) - Maximum number of redirections allowed.
21
21
  - **opts** `object` (required) - Options for handling redirection.
22
22
  - **handler** `object` (required) - Handlers for different stages of the request lifecycle.
@@ -43,8 +43,8 @@ It represents the retry state for a given request.
43
43
 
44
44
  ### Parameter `RetryHandlers`
45
45
 
46
- - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
47
- - **handler** Extends [`Dispatch.DispatchHandlers`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
46
+ - **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
47
+ - **handler** Extends [`Dispatch.DispatchHandler`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
48
48
 
49
49
  >__Note__: The `RetryHandler` does not retry over stateful bodies (e.g. streams, AsyncIterable) as those, once consumed, are left in a state that cannot be reutilized. For these situations the `RetryHandler` will identify
50
50
  >the body as stateful and will not retry the request rejecting with the error `UND_ERR_REQ_RETRY`.
package/index.js CHANGED
@@ -48,6 +48,15 @@ module.exports.cacheStores = {
48
48
  MemoryCacheStore: require('./lib/cache/memory-cache-store')
49
49
  }
50
50
 
51
+ try {
52
+ const SqliteCacheStore = require('./lib/cache/sqlite-cache-store')
53
+ module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore
54
+ } catch (err) {
55
+ if (err.code !== 'ERR_UNKNOWN_BUILTIN_MODULE') {
56
+ throw err
57
+ }
58
+ }
59
+
51
60
  module.exports.buildConnector = buildConnector
52
61
  module.exports.errors = errors
53
62
  module.exports.util = {
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { Writable } = require('node:stream')
4
+ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
4
5
 
5
6
  /**
6
7
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
@@ -70,9 +71,7 @@ class MemoryCacheStore {
70
71
  * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
71
72
  */
72
73
  get (key) {
73
- if (typeof key !== 'object') {
74
- throw new TypeError(`expected key to be object, got ${typeof key}`)
75
- }
74
+ assertCacheKey(key)
76
75
 
77
76
  const topLevelKey = `${key.origin}:${key.path}`
78
77
 
@@ -88,9 +87,11 @@ class MemoryCacheStore {
88
87
  : {
89
88
  statusMessage: entry.statusMessage,
90
89
  statusCode: entry.statusCode,
91
- rawHeaders: entry.rawHeaders,
90
+ headers: entry.headers,
92
91
  body: entry.body,
92
+ vary: entry.vary ? entry.vary : undefined,
93
93
  etag: entry.etag,
94
+ cacheControlDirectives: entry.cacheControlDirectives,
94
95
  cachedAt: entry.cachedAt,
95
96
  staleAt: entry.staleAt,
96
97
  deleteAt: entry.deleteAt
@@ -103,12 +104,8 @@ class MemoryCacheStore {
103
104
  * @returns {Writable | undefined}
104
105
  */
105
106
  createWriteStream (key, val) {
106
- if (typeof key !== 'object') {
107
- throw new TypeError(`expected key to be object, got ${typeof key}`)
108
- }
109
- if (typeof val !== 'object') {
110
- throw new TypeError(`expected value to be object, got ${typeof val}`)
111
- }
107
+ assertCacheKey(key)
108
+ assertCacheValue(val)
112
109
 
113
110
  const topLevelKey = `${key.origin}:${key.path}`
114
111
 
@@ -0,0 +1,446 @@
1
+ 'use strict'
2
+
3
+ const { DatabaseSync } = require('node:sqlite')
4
+ const { Writable } = require('stream')
5
+ const { assertCacheKey, assertCacheValue } = require('../util/cache.js')
6
+
7
+ const VERSION = 3
8
+
9
+ // 2gb
10
+ const MAX_ENTRY_SIZE = 2 * 1000 * 1000 * 1000
11
+
12
+ /**
13
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
14
+ * @implements {CacheStore}
15
+ *
16
+ * @typedef {{
17
+ * id: Readonly<number>
18
+ * headers?: Record<string, string | string[]>
19
+ * vary?: string | object
20
+ * body: string
21
+ * } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
22
+ */
23
+ module.exports = class SqliteCacheStore {
24
+ #maxEntrySize = MAX_ENTRY_SIZE
25
+ #maxCount = Infinity
26
+
27
+ /**
28
+ * @type {import('node:sqlite').DatabaseSync}
29
+ */
30
+ #db
31
+
32
+ /**
33
+ * @type {import('node:sqlite').StatementSync}
34
+ */
35
+ #getValuesQuery
36
+
37
+ /**
38
+ * @type {import('node:sqlite').StatementSync}
39
+ */
40
+ #updateValueQuery
41
+
42
+ /**
43
+ * @type {import('node:sqlite').StatementSync}
44
+ */
45
+ #insertValueQuery
46
+
47
+ /**
48
+ * @type {import('node:sqlite').StatementSync}
49
+ */
50
+ #deleteExpiredValuesQuery
51
+
52
+ /**
53
+ * @type {import('node:sqlite').StatementSync}
54
+ */
55
+ #deleteByUrlQuery
56
+
57
+ /**
58
+ * @type {import('node:sqlite').StatementSync}
59
+ */
60
+ #countEntriesQuery
61
+
62
+ /**
63
+ * @type {import('node:sqlite').StatementSync}
64
+ */
65
+ #deleteOldValuesQuery
66
+
67
+ /**
68
+ * @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
69
+ */
70
+ constructor (opts) {
71
+ if (opts) {
72
+ if (typeof opts !== 'object') {
73
+ throw new TypeError('SqliteCacheStore options must be an object')
74
+ }
75
+
76
+ if (opts.maxEntrySize !== undefined) {
77
+ if (
78
+ typeof opts.maxEntrySize !== 'number' ||
79
+ !Number.isInteger(opts.maxEntrySize) ||
80
+ opts.maxEntrySize < 0
81
+ ) {
82
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
83
+ }
84
+
85
+ if (opts.maxEntrySize > MAX_ENTRY_SIZE) {
86
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be less than 2gb')
87
+ }
88
+
89
+ this.#maxEntrySize = opts.maxEntrySize
90
+ }
91
+
92
+ if (opts.maxCount !== undefined) {
93
+ if (
94
+ typeof opts.maxCount !== 'number' ||
95
+ !Number.isInteger(opts.maxCount) ||
96
+ opts.maxCount < 0
97
+ ) {
98
+ throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
99
+ }
100
+ this.#maxCount = opts.maxCount
101
+ }
102
+ }
103
+
104
+ this.#db = new DatabaseSync(opts?.location ?? ':memory:')
105
+
106
+ this.#db.exec(`
107
+ CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
108
+ -- Data specific to us
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ url TEXT NOT NULL,
111
+ method TEXT NOT NULL,
112
+
113
+ -- Data returned to the interceptor
114
+ body BUF NULL,
115
+ deleteAt INTEGER NOT NULL,
116
+ statusCode INTEGER NOT NULL,
117
+ statusMessage TEXT NOT NULL,
118
+ headers TEXT NULL,
119
+ cacheControlDirectives TEXT NULL,
120
+ etag TEXT NULL,
121
+ vary TEXT NULL,
122
+ cachedAt INTEGER NOT NULL,
123
+ staleAt INTEGER NOT NULL
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_url ON cacheInterceptorV${VERSION}(url);
127
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_method ON cacheInterceptorV${VERSION}(method);
128
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteAt ON cacheInterceptorV${VERSION}(deleteAt);
129
+ `)
130
+
131
+ this.#getValuesQuery = this.#db.prepare(`
132
+ SELECT
133
+ id,
134
+ body,
135
+ deleteAt,
136
+ statusCode,
137
+ statusMessage,
138
+ headers,
139
+ etag,
140
+ cacheControlDirectives,
141
+ vary,
142
+ cachedAt,
143
+ staleAt
144
+ FROM cacheInterceptorV${VERSION}
145
+ WHERE
146
+ url = ?
147
+ AND method = ?
148
+ ORDER BY
149
+ deleteAt ASC
150
+ `)
151
+
152
+ this.#updateValueQuery = this.#db.prepare(`
153
+ UPDATE cacheInterceptorV${VERSION} SET
154
+ body = ?,
155
+ deleteAt = ?,
156
+ statusCode = ?,
157
+ statusMessage = ?,
158
+ headers = ?,
159
+ etag = ?,
160
+ cacheControlDirectives = ?,
161
+ cachedAt = ?,
162
+ staleAt = ?,
163
+ deleteAt = ?
164
+ WHERE
165
+ id = ?
166
+ `)
167
+
168
+ this.#insertValueQuery = this.#db.prepare(`
169
+ INSERT INTO cacheInterceptorV${VERSION} (
170
+ url,
171
+ method,
172
+ body,
173
+ deleteAt,
174
+ statusCode,
175
+ statusMessage,
176
+ headers,
177
+ etag,
178
+ cacheControlDirectives,
179
+ vary,
180
+ cachedAt,
181
+ staleAt,
182
+ deleteAt
183
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
184
+ `)
185
+
186
+ this.#deleteByUrlQuery = this.#db.prepare(
187
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`
188
+ )
189
+
190
+ this.#countEntriesQuery = this.#db.prepare(
191
+ `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`
192
+ )
193
+
194
+ this.#deleteExpiredValuesQuery = this.#db.prepare(
195
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`
196
+ )
197
+
198
+ this.#deleteOldValuesQuery = this.#maxCount === Infinity
199
+ ? null
200
+ : this.#db.prepare(`
201
+ DELETE FROM cacheInterceptorV${VERSION}
202
+ WHERE id IN (
203
+ SELECT
204
+ id
205
+ FROM cacheInterceptorV${VERSION}
206
+ ORDER BY cachedAt DESC
207
+ LIMIT ?
208
+ )
209
+ `)
210
+ }
211
+
212
+ close () {
213
+ this.#db.close()
214
+ }
215
+
216
+ /**
217
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
218
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
219
+ */
220
+ get (key) {
221
+ assertCacheKey(key)
222
+
223
+ const value = this.#findValue(key)
224
+
225
+ if (!value) {
226
+ return undefined
227
+ }
228
+
229
+ /**
230
+ * @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
231
+ */
232
+ const result = {
233
+ body: Buffer.from(value.body),
234
+ statusCode: value.statusCode,
235
+ statusMessage: value.statusMessage,
236
+ headers: value.headers ? JSON.parse(value.headers) : undefined,
237
+ etag: value.etag ? value.etag : undefined,
238
+ vary: value.vary ?? undefined,
239
+ cacheControlDirectives: value.cacheControlDirectives
240
+ ? JSON.parse(value.cacheControlDirectives)
241
+ : undefined,
242
+ cachedAt: value.cachedAt,
243
+ staleAt: value.staleAt,
244
+ deleteAt: value.deleteAt
245
+ }
246
+
247
+ return result
248
+ }
249
+
250
+ /**
251
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
252
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value
253
+ * @returns {Writable | undefined}
254
+ */
255
+ createWriteStream (key, value) {
256
+ assertCacheKey(key)
257
+ assertCacheValue(value)
258
+
259
+ const url = this.#makeValueUrl(key)
260
+ let size = 0
261
+ /**
262
+ * @type {Buffer[] | null}
263
+ */
264
+ const body = []
265
+ const store = this
266
+
267
+ return new Writable({
268
+ write (chunk, encoding, callback) {
269
+ if (typeof chunk === 'string') {
270
+ chunk = Buffer.from(chunk, encoding)
271
+ }
272
+
273
+ size += chunk.byteLength
274
+
275
+ if (size < store.#maxEntrySize) {
276
+ body.push(chunk)
277
+ } else {
278
+ this.destroy()
279
+ }
280
+
281
+ callback()
282
+ },
283
+ final (callback) {
284
+ const existingValue = store.#findValue(key, true)
285
+ if (existingValue) {
286
+ // Updating an existing response, let's overwrite it
287
+ store.#updateValueQuery.run(
288
+ Buffer.concat(body),
289
+ value.deleteAt,
290
+ value.statusCode,
291
+ value.statusMessage,
292
+ value.headers ? JSON.stringify(value.headers) : null,
293
+ value.etag ? value.etag : null,
294
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
295
+ value.cachedAt,
296
+ value.staleAt,
297
+ value.deleteAt,
298
+ existingValue.id
299
+ )
300
+ } else {
301
+ store.#prune()
302
+ // New response, let's insert it
303
+ store.#insertValueQuery.run(
304
+ url,
305
+ key.method,
306
+ Buffer.concat(body),
307
+ value.deleteAt,
308
+ value.statusCode,
309
+ value.statusMessage,
310
+ value.headers ? JSON.stringify(value.headers) : null,
311
+ value.etag ? value.etag : null,
312
+ value.cacheControlDirectives ? JSON.stringify(value.cacheControlDirectives) : null,
313
+ value.vary ? JSON.stringify(value.vary) : null,
314
+ value.cachedAt,
315
+ value.staleAt,
316
+ value.deleteAt
317
+ )
318
+ }
319
+
320
+ callback()
321
+ }
322
+ })
323
+ }
324
+
325
+ /**
326
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
327
+ */
328
+ delete (key) {
329
+ if (typeof key !== 'object') {
330
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
331
+ }
332
+
333
+ this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
334
+ }
335
+
336
+ #prune () {
337
+ if (this.size <= this.#maxCount) {
338
+ return 0
339
+ }
340
+
341
+ {
342
+ const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
343
+ if (removed > 0) {
344
+ return removed
345
+ }
346
+ }
347
+
348
+ {
349
+ const removed = this.#deleteOldValuesQuery.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
350
+ if (removed > 0) {
351
+ return removed
352
+ }
353
+ }
354
+
355
+ return 0
356
+ }
357
+
358
+ /**
359
+ * Counts the number of rows in the cache
360
+ * @returns {Number}
361
+ */
362
+ get size () {
363
+ const { total } = this.#countEntriesQuery.get()
364
+ return total
365
+ }
366
+
367
+ /**
368
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
369
+ * @returns {string}
370
+ */
371
+ #makeValueUrl (key) {
372
+ return `${key.origin}/${key.path}`
373
+ }
374
+
375
+ /**
376
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
377
+ * @param {boolean} [canBeExpired=false]
378
+ * @returns {(SqliteStoreValue & { vary?: Record<string, string[]> }) | undefined}
379
+ */
380
+ #findValue (key, canBeExpired = false) {
381
+ const url = this.#makeValueUrl(key)
382
+ const { headers, method } = key
383
+
384
+ /**
385
+ * @type {SqliteStoreValue[]}
386
+ */
387
+ const values = this.#getValuesQuery.all(url, method)
388
+
389
+ if (values.length === 0) {
390
+ return undefined
391
+ }
392
+
393
+ const now = Date.now()
394
+ for (const value of values) {
395
+ if (now >= value.deleteAt && !canBeExpired) {
396
+ return undefined
397
+ }
398
+
399
+ let matches = true
400
+
401
+ if (value.vary) {
402
+ if (!headers) {
403
+ return undefined
404
+ }
405
+
406
+ value.vary = JSON.parse(value.vary)
407
+
408
+ for (const header in value.vary) {
409
+ if (!headerValueEquals(headers[header], value.vary[header])) {
410
+ matches = false
411
+ break
412
+ }
413
+ }
414
+ }
415
+
416
+ if (matches) {
417
+ return value
418
+ }
419
+ }
420
+
421
+ return undefined
422
+ }
423
+ }
424
+
425
+ /**
426
+ * @param {string|string[]|null|undefined} lhs
427
+ * @param {string|string[]|null|undefined} rhs
428
+ * @returns {boolean}
429
+ */
430
+ function headerValueEquals (lhs, rhs) {
431
+ if (Array.isArray(lhs) && Array.isArray(rhs)) {
432
+ if (lhs.length !== rhs.length) {
433
+ return false
434
+ }
435
+
436
+ for (let i = 0; i < lhs.length; i++) {
437
+ if (rhs.includes(lhs[i])) {
438
+ return false
439
+ }
440
+ }
441
+
442
+ return true
443
+ }
444
+
445
+ return lhs === rhs
446
+ }
package/lib/core/util.js CHANGED
@@ -511,6 +511,11 @@ function assertRequestHandler (handler, method, upgrade) {
511
511
  throw new InvalidArgumentError('handler must be an object')
512
512
  }
513
513
 
514
+ if (typeof handler.onRequestStart === 'function') {
515
+ // TODO (fix): More checks...
516
+ return
517
+ }
518
+
514
519
  if (typeof handler.onConnect !== 'function') {
515
520
  throw new InvalidArgumentError('invalid onConnect method')
516
521
  }
@@ -769,8 +769,19 @@ async function connectH1 (client, socket) {
769
769
  client[kSocket] = socket
770
770
 
771
771
  if (!llhttpInstance) {
772
+ const noop = () => {}
773
+ socket.on('error', noop)
772
774
  llhttpInstance = await llhttpPromise
773
775
  llhttpPromise = null
776
+ socket.off('error', noop)
777
+ }
778
+
779
+ if (socket.errored) {
780
+ throw socket.errored
781
+ }
782
+
783
+ if (socket.destroyed) {
784
+ throw new SocketError('destroyed')
774
785
  }
775
786
 
776
787
  socket[kNoRef] = false
@@ -32,6 +32,8 @@ const {
32
32
 
33
33
  const kOpenStreams = Symbol('open streams')
34
34
 
35
+ let extractBody
36
+
35
37
  // Experimental
36
38
  let h2ExperimentalWarned = false
37
39
 
@@ -201,11 +203,12 @@ function onHttp2SessionGoAway (errorCode) {
201
203
  util.destroy(this[kSocket], err)
202
204
 
203
205
  // Fail head of pipeline.
204
- const request = client[kQueue][client[kRunningIdx]]
205
- client[kQueue][client[kRunningIdx]++] = null
206
- util.errorRequest(client, request, err)
207
-
208
- client[kPendingIdx] = client[kRunningIdx]
206
+ if (client[kRunningIdx] < client[kQueue].length) {
207
+ const request = client[kQueue][client[kRunningIdx]]
208
+ client[kQueue][client[kRunningIdx]++] = null
209
+ util.errorRequest(client, request, err)
210
+ client[kPendingIdx] = client[kRunningIdx]
211
+ }
209
212
 
210
213
  assert(client[kRunning] === 0)
211
214
 
@@ -279,7 +282,8 @@ function shouldSendContentLength (method) {
279
282
 
280
283
  function writeH2 (client, request) {
281
284
  const session = client[kHTTP2Session]
282
- const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
285
+ const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
286
+ let { body } = request
283
287
 
284
288
  if (upgrade) {
285
289
  util.errorRequest(client, request, new Error('Upgrade not supported for H2'))
@@ -407,6 +411,16 @@ function writeH2 (client, request) {
407
411
 
408
412
  let contentLength = util.bodyLength(body)
409
413
 
414
+ if (util.isFormDataLike(body)) {
415
+ extractBody ??= require('../web/fetch/body.js').extractBody
416
+
417
+ const [bodyStream, contentType] = extractBody(body)
418
+ headers['content-type'] = contentType
419
+
420
+ body = bodyStream.stream
421
+ contentLength = bodyStream.length
422
+ }
423
+
410
424
  if (contentLength == null) {
411
425
  contentLength = request.contentLength
412
426
  }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const Dispatcher = require('./dispatcher')
4
+ const UnwrapHandler = require('../handler/unwrap-handler')
4
5
  const {
5
6
  ClientDestroyedError,
6
7
  ClientClosedError,
@@ -142,7 +143,7 @@ class DispatcherBase extends Dispatcher {
142
143
  throw new ClientClosedError()
143
144
  }
144
145
 
145
- return this[kDispatch](opts, handler)
146
+ return this[kDispatch](opts, UnwrapHandler.unwrap(handler))
146
147
  } catch (err) {
147
148
  if (typeof handler.onError !== 'function') {
148
149
  throw new InvalidArgumentError('invalid onError method')