undici 7.0.0-alpha.6 → 7.0.0-alpha.7

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
 
@@ -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,7 +87,7 @@ 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,
93
92
  etag: entry.etag,
94
93
  cachedAt: entry.cachedAt,
@@ -103,12 +102,8 @@ class MemoryCacheStore {
103
102
  * @returns {Writable | undefined}
104
103
  */
105
104
  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
- }
105
+ assertCacheKey(key)
106
+ assertCacheValue(val)
112
107
 
113
108
  const topLevelKey = `${key.origin}:${key.path}`
114
109
 
@@ -0,0 +1,457 @@
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 = 2
8
+
9
+ /**
10
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
11
+ * @implements {CacheStore}
12
+ *
13
+ * @typedef {{
14
+ * id: Readonly<number>
15
+ * headers?: Record<string, string | string[]>
16
+ * vary?: string | object
17
+ * body: string
18
+ * } & import('../../types/cache-interceptor.d.ts').default.CacheValue} SqliteStoreValue
19
+ */
20
+ class SqliteCacheStore {
21
+ #maxEntrySize = Infinity
22
+ #maxCount = Infinity
23
+
24
+ /**
25
+ * @type {import('node:sqlite').DatabaseSync}
26
+ */
27
+ #db
28
+
29
+ /**
30
+ * @type {import('node:sqlite').StatementSync}
31
+ */
32
+ #getValuesQuery
33
+
34
+ /**
35
+ * @type {import('node:sqlite').StatementSync}
36
+ */
37
+ #updateValueQuery
38
+
39
+ /**
40
+ * @type {import('node:sqlite').StatementSync}
41
+ */
42
+ #insertValueQuery
43
+
44
+ /**
45
+ * @type {import('node:sqlite').StatementSync}
46
+ */
47
+ #deleteExpiredValuesQuery
48
+
49
+ /**
50
+ * @type {import('node:sqlite').StatementSync}
51
+ */
52
+ #deleteByUrlQuery
53
+
54
+ /**
55
+ * @type {import('node:sqlite').StatementSync}
56
+ */
57
+ #countEntriesQuery
58
+
59
+ /**
60
+ * @type {import('node:sqlite').StatementSync}
61
+ */
62
+ #deleteOldValuesQuery
63
+
64
+ /**
65
+ * @param {import('../../types/cache-interceptor.d.ts').default.SqliteCacheStoreOpts | undefined} opts
66
+ */
67
+ constructor (opts) {
68
+ if (opts) {
69
+ if (typeof opts !== 'object') {
70
+ throw new TypeError('SqliteCacheStore options must be an object')
71
+ }
72
+
73
+ if (opts.maxEntrySize !== undefined) {
74
+ if (
75
+ typeof opts.maxEntrySize !== 'number' ||
76
+ !Number.isInteger(opts.maxEntrySize) ||
77
+ opts.maxEntrySize < 0
78
+ ) {
79
+ throw new TypeError('SqliteCacheStore options.maxEntrySize must be a non-negative integer')
80
+ }
81
+ this.#maxEntrySize = opts.maxEntrySize
82
+ }
83
+
84
+ if (opts.maxCount !== undefined) {
85
+ if (
86
+ typeof opts.maxCount !== 'number' ||
87
+ !Number.isInteger(opts.maxCount) ||
88
+ opts.maxCount < 0
89
+ ) {
90
+ throw new TypeError('SqliteCacheStore options.maxCount must be a non-negative integer')
91
+ }
92
+ this.#maxCount = opts.maxCount
93
+ }
94
+ }
95
+
96
+ this.#db = new DatabaseSync(opts?.location ?? ':memory:')
97
+
98
+ this.#db.exec(`
99
+ CREATE TABLE IF NOT EXISTS cacheInterceptorV${VERSION} (
100
+ -- Data specific to us
101
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
102
+ url TEXT NOT NULL,
103
+ method TEXT NOT NULL,
104
+
105
+ -- Data returned to the interceptor
106
+ body TEXT NULL,
107
+ deleteAt INTEGER NOT NULL,
108
+ statusCode INTEGER NOT NULL,
109
+ statusMessage TEXT NOT NULL,
110
+ headers TEXT NULL,
111
+ etag TEXT NULL,
112
+ vary TEXT NULL,
113
+ cachedAt INTEGER NOT NULL,
114
+ staleAt INTEGER NOT NULL
115
+ );
116
+
117
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_url ON cacheInterceptorV${VERSION}(url);
118
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_method ON cacheInterceptorV${VERSION}(method);
119
+ CREATE INDEX IF NOT EXISTS idx_cacheInterceptorV${VERSION}_deleteAt ON cacheInterceptorV${VERSION}(deleteAt);
120
+ `)
121
+
122
+ this.#getValuesQuery = this.#db.prepare(`
123
+ SELECT
124
+ id,
125
+ body,
126
+ deleteAt,
127
+ statusCode,
128
+ statusMessage,
129
+ headers,
130
+ etag,
131
+ vary,
132
+ cachedAt,
133
+ staleAt
134
+ FROM cacheInterceptorV${VERSION}
135
+ WHERE
136
+ url = ?
137
+ AND method = ?
138
+ ORDER BY
139
+ deleteAt ASC
140
+ `)
141
+
142
+ this.#updateValueQuery = this.#db.prepare(`
143
+ UPDATE cacheInterceptorV${VERSION} SET
144
+ body = ?,
145
+ deleteAt = ?,
146
+ statusCode = ?,
147
+ statusMessage = ?,
148
+ headers = ?,
149
+ etag = ?,
150
+ cachedAt = ?,
151
+ staleAt = ?,
152
+ deleteAt = ?
153
+ WHERE
154
+ id = ?
155
+ `)
156
+
157
+ this.#insertValueQuery = this.#db.prepare(`
158
+ INSERT INTO cacheInterceptorV${VERSION} (
159
+ url,
160
+ method,
161
+ body,
162
+ deleteAt,
163
+ statusCode,
164
+ statusMessage,
165
+ headers,
166
+ etag,
167
+ vary,
168
+ cachedAt,
169
+ staleAt,
170
+ deleteAt
171
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
172
+ `)
173
+
174
+ this.#deleteByUrlQuery = this.#db.prepare(
175
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE url = ?`
176
+ )
177
+
178
+ this.#countEntriesQuery = this.#db.prepare(
179
+ `SELECT COUNT(*) AS total FROM cacheInterceptorV${VERSION}`
180
+ )
181
+
182
+ this.#deleteExpiredValuesQuery = this.#db.prepare(
183
+ `DELETE FROM cacheInterceptorV${VERSION} WHERE deleteAt <= ?`
184
+ )
185
+
186
+ this.#deleteOldValuesQuery = this.#maxCount === Infinity
187
+ ? null
188
+ : this.#db.prepare(`
189
+ DELETE FROM cacheInterceptorV${VERSION}
190
+ WHERE id IN (
191
+ SELECT
192
+ id
193
+ FROM cacheInterceptorV${VERSION}
194
+ ORDER BY cachedAt DESC
195
+ LIMIT ?
196
+ )
197
+ `)
198
+ }
199
+
200
+ close () {
201
+ this.#db.close()
202
+ }
203
+
204
+ /**
205
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
206
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
207
+ */
208
+ get (key) {
209
+ assertCacheKey(key)
210
+
211
+ const value = this.#findValue(key)
212
+
213
+ if (!value) {
214
+ return undefined
215
+ }
216
+
217
+ /**
218
+ * @type {import('../../types/cache-interceptor.d.ts').default.GetResult}
219
+ */
220
+ const result = {
221
+ body: value.body ? parseBufferArray(JSON.parse(value.body)) : null,
222
+ statusCode: value.statusCode,
223
+ statusMessage: value.statusMessage,
224
+ headers: value.headers ? JSON.parse(value.headers) : undefined,
225
+ etag: value.etag ? value.etag : undefined,
226
+ cachedAt: value.cachedAt,
227
+ staleAt: value.staleAt,
228
+ deleteAt: value.deleteAt
229
+ }
230
+
231
+ return result
232
+ }
233
+
234
+ /**
235
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
236
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheValue} value
237
+ * @returns {Writable | undefined}
238
+ */
239
+ createWriteStream (key, value) {
240
+ assertCacheKey(key)
241
+ assertCacheValue(value)
242
+
243
+ const url = this.#makeValueUrl(key)
244
+ let size = 0
245
+ /**
246
+ * @type {Buffer[] | null}
247
+ */
248
+ const body = []
249
+ const store = this
250
+
251
+ return new Writable({
252
+ write (chunk, encoding, callback) {
253
+ if (typeof chunk === 'string') {
254
+ chunk = Buffer.from(chunk, encoding)
255
+ }
256
+
257
+ size += chunk.byteLength
258
+
259
+ if (size < store.#maxEntrySize) {
260
+ body.push(chunk)
261
+ } else {
262
+ this.destroy()
263
+ }
264
+
265
+ callback()
266
+ },
267
+ final (callback) {
268
+ const existingValue = store.#findValue(key, true)
269
+ if (existingValue) {
270
+ // Updating an existing response, let's overwrite it
271
+ store.#updateValueQuery.run(
272
+ JSON.stringify(stringifyBufferArray(body)),
273
+ value.deleteAt,
274
+ value.statusCode,
275
+ value.statusMessage,
276
+ value.headers ? JSON.stringify(value.headers) : null,
277
+ value.etag,
278
+ value.cachedAt,
279
+ value.staleAt,
280
+ value.deleteAt,
281
+ existingValue.id
282
+ )
283
+ } else {
284
+ store.#prune()
285
+ // New response, let's insert it
286
+ store.#insertValueQuery.run(
287
+ url,
288
+ key.method,
289
+ JSON.stringify(stringifyBufferArray(body)),
290
+ value.deleteAt,
291
+ value.statusCode,
292
+ value.statusMessage,
293
+ value.headers ? JSON.stringify(value.headers) : null,
294
+ value.etag ? value.etag : null,
295
+ value.vary ? JSON.stringify(value.vary) : null,
296
+ value.cachedAt,
297
+ value.staleAt,
298
+ value.deleteAt
299
+ )
300
+ }
301
+
302
+ callback()
303
+ }
304
+ })
305
+ }
306
+
307
+ /**
308
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
309
+ */
310
+ delete (key) {
311
+ if (typeof key !== 'object') {
312
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
313
+ }
314
+
315
+ this.#deleteByUrlQuery.run(this.#makeValueUrl(key))
316
+ }
317
+
318
+ #prune () {
319
+ if (this.#size <= this.#maxCount) {
320
+ return 0
321
+ }
322
+
323
+ {
324
+ const removed = this.#deleteExpiredValuesQuery.run(Date.now()).changes
325
+ if (removed > 0) {
326
+ return removed
327
+ }
328
+ }
329
+
330
+ {
331
+ const removed = this.#deleteOldValuesQuery.run(Math.max(Math.floor(this.#maxCount * 0.1), 1)).changes
332
+ if (removed > 0) {
333
+ return removed
334
+ }
335
+ }
336
+
337
+ return 0
338
+ }
339
+
340
+ /**
341
+ * Counts the number of rows in the cache
342
+ * @returns {Number}
343
+ */
344
+ get #size () {
345
+ const { total } = this.#countEntriesQuery.get()
346
+ return total
347
+ }
348
+
349
+ /**
350
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
351
+ * @returns {string}
352
+ */
353
+ #makeValueUrl (key) {
354
+ return `${key.origin}/${key.path}`
355
+ }
356
+
357
+ /**
358
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
359
+ * @param {boolean} [canBeExpired=false]
360
+ * @returns {(SqliteStoreValue & { vary?: Record<string, string[]> }) | undefined}
361
+ */
362
+ #findValue (key, canBeExpired = false) {
363
+ const url = this.#makeValueUrl(key)
364
+ const { headers, method } = key
365
+
366
+ /**
367
+ * @type {SqliteStoreValue[]}
368
+ */
369
+ const values = this.#getValuesQuery.all(url, method)
370
+
371
+ if (values.length === 0) {
372
+ return undefined
373
+ }
374
+
375
+ const now = Date.now()
376
+ for (const value of values) {
377
+ if (now >= value.deleteAt && !canBeExpired) {
378
+ return undefined
379
+ }
380
+
381
+ let matches = true
382
+
383
+ if (value.vary) {
384
+ if (!headers) {
385
+ return undefined
386
+ }
387
+
388
+ const vary = JSON.parse(value.vary)
389
+
390
+ for (const header in vary) {
391
+ if (headerValueEquals(headers[header], vary[header])) {
392
+ matches = false
393
+ break
394
+ }
395
+ }
396
+ }
397
+
398
+ if (matches) {
399
+ return value
400
+ }
401
+ }
402
+
403
+ return undefined
404
+ }
405
+ }
406
+
407
+ /**
408
+ * @param {string|string[]|null|undefined} lhs
409
+ * @param {string|string[]|null|undefined} rhs
410
+ * @returns {boolean}
411
+ */
412
+ function headerValueEquals (lhs, rhs) {
413
+ if (Array.isArray(lhs) && Array.isArray(rhs)) {
414
+ if (lhs.length !== rhs.length) {
415
+ return false
416
+ }
417
+
418
+ for (let i = 0; i < lhs.length; i++) {
419
+ if (rhs.includes(lhs[i])) {
420
+ return false
421
+ }
422
+ }
423
+
424
+ return true
425
+ }
426
+
427
+ return lhs === rhs
428
+ }
429
+
430
+ /**
431
+ * @param {Buffer[]} buffers
432
+ * @returns {string[]}
433
+ */
434
+ function stringifyBufferArray (buffers) {
435
+ const output = new Array(buffers.length)
436
+ for (let i = 0; i < buffers.length; i++) {
437
+ output[i] = buffers[i].toString()
438
+ }
439
+
440
+ return output
441
+ }
442
+
443
+ /**
444
+ * @param {string[]} strings
445
+ * @returns {Buffer[]}
446
+ */
447
+ function parseBufferArray (strings) {
448
+ const output = new Array(strings.length)
449
+
450
+ for (let i = 0; i < strings.length; i++) {
451
+ output[i] = Buffer.from(strings[i])
452
+ }
453
+
454
+ return output
455
+ }
456
+
457
+ module.exports = SqliteCacheStore
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')