undici 7.0.0-alpha.2 → 7.0.0-alpha.4

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 (56) hide show
  1. package/README.md +3 -2
  2. package/docs/docs/api/BalancedPool.md +1 -1
  3. package/docs/docs/api/CacheStore.md +100 -0
  4. package/docs/docs/api/Dispatcher.md +32 -2
  5. package/docs/docs/api/MockClient.md +1 -1
  6. package/docs/docs/api/Pool.md +1 -1
  7. package/docs/docs/api/api-lifecycle.md +2 -2
  8. package/docs/docs/best-practices/mocking-request.md +2 -2
  9. package/docs/docs/best-practices/proxy.md +1 -1
  10. package/index.d.ts +1 -1
  11. package/index.js +8 -2
  12. package/lib/api/api-request.js +2 -2
  13. package/lib/api/readable.js +6 -6
  14. package/lib/cache/memory-cache-store.js +325 -0
  15. package/lib/core/connect.js +5 -0
  16. package/lib/core/constants.js +24 -1
  17. package/lib/core/request.js +2 -2
  18. package/lib/core/util.js +13 -1
  19. package/lib/dispatcher/client-h1.js +100 -87
  20. package/lib/dispatcher/client-h2.js +168 -96
  21. package/lib/dispatcher/pool-base.js +3 -3
  22. package/lib/handler/cache-handler.js +389 -0
  23. package/lib/handler/cache-revalidation-handler.js +151 -0
  24. package/lib/handler/redirect-handler.js +5 -3
  25. package/lib/handler/retry-handler.js +3 -3
  26. package/lib/interceptor/cache.js +192 -0
  27. package/lib/interceptor/dns.js +71 -48
  28. package/lib/util/cache.js +249 -0
  29. package/lib/web/cache/cache.js +1 -0
  30. package/lib/web/cache/cachestorage.js +2 -0
  31. package/lib/web/cookies/index.js +12 -1
  32. package/lib/web/cookies/parse.js +6 -1
  33. package/lib/web/eventsource/eventsource.js +2 -0
  34. package/lib/web/fetch/body.js +1 -5
  35. package/lib/web/fetch/constants.js +12 -5
  36. package/lib/web/fetch/data-url.js +2 -2
  37. package/lib/web/fetch/formdata-parser.js +70 -43
  38. package/lib/web/fetch/formdata.js +3 -1
  39. package/lib/web/fetch/headers.js +3 -1
  40. package/lib/web/fetch/index.js +4 -6
  41. package/lib/web/fetch/request.js +3 -1
  42. package/lib/web/fetch/response.js +3 -1
  43. package/lib/web/fetch/util.js +171 -47
  44. package/lib/web/fetch/webidl.js +28 -16
  45. package/lib/web/websocket/constants.js +67 -6
  46. package/lib/web/websocket/events.js +4 -0
  47. package/lib/web/websocket/stream/websocketerror.js +1 -1
  48. package/lib/web/websocket/websocket.js +2 -0
  49. package/package.json +8 -5
  50. package/types/cache-interceptor.d.ts +101 -0
  51. package/types/cookies.d.ts +2 -0
  52. package/types/dispatcher.d.ts +1 -1
  53. package/types/fetch.d.ts +9 -8
  54. package/types/index.d.ts +3 -1
  55. package/types/interceptors.d.ts +4 -1
  56. package/types/webidl.d.ts +7 -1
@@ -0,0 +1,325 @@
1
+ 'use strict'
2
+
3
+ const { Writable } = require('node:stream')
4
+
5
+ /**
6
+ * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheStore} CacheStore
7
+ * @implements {CacheStore}
8
+ *
9
+ * @typedef {{
10
+ * locked: boolean
11
+ * opts: import('../../types/cache-interceptor.d.ts').default.CachedResponse
12
+ * body?: Buffer[]
13
+ * }} MemoryStoreValue
14
+ */
15
+ class MemoryCacheStore {
16
+ #maxCount = Infinity
17
+
18
+ #maxEntrySize = Infinity
19
+
20
+ #entryCount = 0
21
+
22
+ /**
23
+ * @type {Map<string, Map<string, MemoryStoreValue[]>>}
24
+ */
25
+ #data = new Map()
26
+
27
+ /**
28
+ * @param {import('../../types/cache-interceptor.d.ts').default.MemoryCacheStoreOpts | undefined} [opts]
29
+ */
30
+ constructor (opts) {
31
+ if (opts) {
32
+ if (typeof opts !== 'object') {
33
+ throw new TypeError('MemoryCacheStore options must be an object')
34
+ }
35
+
36
+ if (opts.maxCount !== undefined) {
37
+ if (
38
+ typeof opts.maxCount !== 'number' ||
39
+ !Number.isInteger(opts.maxCount) ||
40
+ opts.maxCount < 0
41
+ ) {
42
+ throw new TypeError('MemoryCacheStore options.maxCount must be a non-negative integer')
43
+ }
44
+ this.#maxCount = opts.maxCount
45
+ }
46
+
47
+ if (opts.maxEntrySize !== undefined) {
48
+ if (
49
+ typeof opts.maxEntrySize !== 'number' ||
50
+ !Number.isInteger(opts.maxEntrySize) ||
51
+ opts.maxEntrySize < 0
52
+ ) {
53
+ throw new TypeError('MemoryCacheStore options.maxEntrySize must be a non-negative integer')
54
+ }
55
+ this.#maxEntrySize = opts.maxEntrySize
56
+ }
57
+ }
58
+ }
59
+
60
+ get isFull () {
61
+ return this.#entryCount >= this.#maxCount
62
+ }
63
+
64
+ /**
65
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
66
+ * @returns {import('../../types/cache-interceptor.d.ts').default.GetResult | undefined}
67
+ */
68
+ get (key) {
69
+ if (typeof key !== 'object') {
70
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
71
+ }
72
+
73
+ const values = this.#getValuesForRequest(key, false)
74
+ if (!values) {
75
+ return undefined
76
+ }
77
+
78
+ const value = this.#findValue(key, values)
79
+
80
+ if (!value || value.locked) {
81
+ return undefined
82
+ }
83
+
84
+ return { ...value.opts, body: value.body }
85
+ }
86
+
87
+ /**
88
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
89
+ * @param {import('../../types/cache-interceptor.d.ts').default.CachedResponse} opts
90
+ * @returns {Writable | undefined}
91
+ */
92
+ createWriteStream (key, opts) {
93
+ if (typeof key !== 'object') {
94
+ throw new TypeError(`expected key to be object, got ${typeof key}`)
95
+ }
96
+ if (typeof opts !== 'object') {
97
+ throw new TypeError(`expected value to be object, got ${typeof opts}`)
98
+ }
99
+
100
+ if (this.isFull) {
101
+ return undefined
102
+ }
103
+
104
+ const values = this.#getValuesForRequest(key, true)
105
+
106
+ /**
107
+ * @type {(MemoryStoreValue & { index: number }) | undefined}
108
+ */
109
+ let value = this.#findValue(key, values)
110
+ let valueIndex = value?.index
111
+ if (!value) {
112
+ // The value doesn't already exist, meaning we haven't cached this
113
+ // response before. Let's assign it a value and insert it into our data
114
+ // property.
115
+
116
+ if (this.isFull) {
117
+ // Or not, we don't have space to add another response
118
+ return undefined
119
+ }
120
+
121
+ this.#entryCount++
122
+
123
+ value = {
124
+ locked: true,
125
+ opts
126
+ }
127
+
128
+ // We want to sort our responses in decending order by their deleteAt
129
+ // timestamps so that deleting expired responses is faster
130
+ if (
131
+ values.length === 0 ||
132
+ opts.deleteAt < values[values.length - 1].deleteAt
133
+ ) {
134
+ // Our value is either the only response for this path or our deleteAt
135
+ // time is sooner than all the other responses
136
+ values.push(value)
137
+ valueIndex = values.length - 1
138
+ } else if (opts.deleteAt >= values[0].deleteAt) {
139
+ // Our deleteAt is later than everyone elses
140
+ values.unshift(value)
141
+ valueIndex = 0
142
+ } else {
143
+ // We're neither in the front or the end, let's just binary search to
144
+ // find our stop we need to be in
145
+ let startIndex = 0
146
+ let endIndex = values.length
147
+ while (true) {
148
+ if (startIndex === endIndex) {
149
+ values.splice(startIndex, 0, value)
150
+ break
151
+ }
152
+
153
+ const middleIndex = Math.floor((startIndex + endIndex) / 2)
154
+ const middleValue = values[middleIndex]
155
+ if (opts.deleteAt === middleIndex) {
156
+ values.splice(middleIndex, 0, value)
157
+ valueIndex = middleIndex
158
+ break
159
+ } else if (opts.deleteAt > middleValue.opts.deleteAt) {
160
+ endIndex = middleIndex
161
+ continue
162
+ } else {
163
+ startIndex = middleIndex
164
+ continue
165
+ }
166
+ }
167
+ }
168
+ } else {
169
+ // Check if there's already another request writing to the value or
170
+ // a request reading from it
171
+ if (value.locked) {
172
+ return undefined
173
+ }
174
+
175
+ // Empty it so we can overwrite it
176
+ value.body = []
177
+ }
178
+
179
+ let currentSize = 0
180
+ /**
181
+ * @type {Buffer[] | null}
182
+ */
183
+ let body = key.method !== 'HEAD' ? [] : null
184
+ const maxEntrySize = this.#maxEntrySize
185
+
186
+ const writable = new Writable({
187
+ write (chunk, encoding, callback) {
188
+ if (key.method === 'HEAD') {
189
+ throw new Error('HEAD request shouldn\'t have a body')
190
+ }
191
+
192
+ if (!body) {
193
+ return callback()
194
+ }
195
+
196
+ if (typeof chunk === 'string') {
197
+ chunk = Buffer.from(chunk, encoding)
198
+ }
199
+
200
+ currentSize += chunk.byteLength
201
+
202
+ if (currentSize >= maxEntrySize) {
203
+ body = null
204
+ this.end()
205
+ shiftAtIndex(values, valueIndex)
206
+ return callback()
207
+ }
208
+
209
+ body.push(chunk)
210
+ callback()
211
+ },
212
+ final (callback) {
213
+ value.locked = false
214
+ if (body !== null) {
215
+ value.body = body
216
+ }
217
+
218
+ callback()
219
+ }
220
+ })
221
+
222
+ return writable
223
+ }
224
+
225
+ /**
226
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
227
+ */
228
+ delete (key) {
229
+ this.#data.delete(`${key.origin}:${key.path}`)
230
+ }
231
+
232
+ /**
233
+ * Gets all of the requests of the same origin, path, and method. Does not
234
+ * take the `vary` property into account.
235
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} key
236
+ * @param {boolean} [makeIfDoesntExist=false]
237
+ * @returns {MemoryStoreValue[] | undefined}
238
+ */
239
+ #getValuesForRequest (key, makeIfDoesntExist) {
240
+ // https://www.rfc-editor.org/rfc/rfc9111.html#section-2-3
241
+ const topLevelKey = `${key.origin}:${key.path}`
242
+ let cachedPaths = this.#data.get(topLevelKey)
243
+ if (!cachedPaths) {
244
+ if (!makeIfDoesntExist) {
245
+ return undefined
246
+ }
247
+
248
+ cachedPaths = new Map()
249
+ this.#data.set(topLevelKey, cachedPaths)
250
+ }
251
+
252
+ let value = cachedPaths.get(key.method)
253
+ if (!value && makeIfDoesntExist) {
254
+ value = []
255
+ cachedPaths.set(key.method, value)
256
+ }
257
+
258
+ return value
259
+ }
260
+
261
+ /**
262
+ * Given a list of values of a certain request, this decides the best value
263
+ * to respond with.
264
+ * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} req
265
+ * @param {MemoryStoreValue[]} values
266
+ * @returns {(MemoryStoreValue & { index: number }) | undefined}
267
+ */
268
+ #findValue (req, values) {
269
+ /**
270
+ * @type {MemoryStoreValue | undefined}
271
+ */
272
+ let value
273
+ const now = Date.now()
274
+ for (let i = values.length - 1; i >= 0; i--) {
275
+ const current = values[i]
276
+ const currentCacheValue = current.opts
277
+ if (now >= currentCacheValue.deleteAt) {
278
+ // We've reached expired values, let's delete them
279
+ this.#entryCount -= values.length - i
280
+ values.length = i
281
+ break
282
+ }
283
+
284
+ let matches = true
285
+
286
+ if (currentCacheValue.vary) {
287
+ if (!req.headers) {
288
+ matches = false
289
+ break
290
+ }
291
+
292
+ for (const key in currentCacheValue.vary) {
293
+ if (currentCacheValue.vary[key] !== req.headers[key]) {
294
+ matches = false
295
+ break
296
+ }
297
+ }
298
+ }
299
+
300
+ if (matches) {
301
+ value = {
302
+ ...current,
303
+ index: i
304
+ }
305
+ break
306
+ }
307
+ }
308
+
309
+ return value
310
+ }
311
+ }
312
+
313
+ /**
314
+ * @param {any[]} array Array to modify
315
+ * @param {number} idx Index to delete
316
+ */
317
+ function shiftAtIndex (array, idx) {
318
+ for (let i = idx + 1; idx < array.length; i++) {
319
+ array[i - 1] = array[i]
320
+ }
321
+
322
+ array.length--
323
+ }
324
+
325
+ module.exports = MemoryCacheStore
@@ -220,6 +220,11 @@ const setupConnectTimeout = process.platform === 'win32'
220
220
  * @param {number} opts.port
221
221
  */
222
222
  function onConnectTimeout (socket, opts) {
223
+ // The socket could be already garbage collected
224
+ if (socket == null) {
225
+ return
226
+ }
227
+
223
228
  let message = 'Connect Timeout Error'
224
229
  if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
225
230
  message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
@@ -107,6 +107,28 @@ const headerNameLowerCasedRecord = {}
107
107
  // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
108
108
  Object.setPrototypeOf(headerNameLowerCasedRecord, null)
109
109
 
110
+ /**
111
+ * @type {Record<Lowercase<typeof wellknownHeaderNames[number]>, Buffer>}
112
+ */
113
+ const wellknownHeaderNameBuffers = {}
114
+
115
+ // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
116
+ Object.setPrototypeOf(wellknownHeaderNameBuffers, null)
117
+
118
+ /**
119
+ * @param {string} header Lowercased header
120
+ * @returns {Buffer}
121
+ */
122
+ function getHeaderNameAsBuffer (header) {
123
+ let buffer = wellknownHeaderNameBuffers[header]
124
+
125
+ if (buffer === undefined) {
126
+ buffer = Buffer.from(header)
127
+ }
128
+
129
+ return buffer
130
+ }
131
+
110
132
  for (let i = 0; i < wellknownHeaderNames.length; ++i) {
111
133
  const key = wellknownHeaderNames[i]
112
134
  const lowerCasedKey = key.toLowerCase()
@@ -116,5 +138,6 @@ for (let i = 0; i < wellknownHeaderNames.length; ++i) {
116
138
 
117
139
  module.exports = {
118
140
  wellknownHeaderNames,
119
- headerNameLowerCasedRecord
141
+ headerNameLowerCasedRecord,
142
+ getHeaderNameAsBuffer
120
143
  }
@@ -130,7 +130,6 @@ class Request {
130
130
  }
131
131
 
132
132
  this.completed = false
133
-
134
133
  this.aborted = false
135
134
 
136
135
  this.upgrade = upgrade || null
@@ -143,7 +142,7 @@ class Request {
143
142
  ? method === 'HEAD' || method === 'GET'
144
143
  : idempotent
145
144
 
146
- this.blocking = blocking == null ? false : blocking
145
+ this.blocking = blocking ?? this.method !== 'HEAD'
147
146
 
148
147
  this.reset = reset == null ? null : reset
149
148
 
@@ -272,6 +271,7 @@ class Request {
272
271
  this.onFinally()
273
272
 
274
273
  assert(!this.aborted)
274
+ assert(!this.completed)
275
275
 
276
276
  this.completed = true
277
277
  if (channels.trailers.hasSubscribers) {
package/lib/core/util.js CHANGED
@@ -478,6 +478,17 @@ function parseRawHeaders (headers) {
478
478
  return ret
479
479
  }
480
480
 
481
+ /**
482
+ * @param {string[]} headers
483
+ * @param {Buffer[]} headers
484
+ */
485
+ function encodeRawHeaders (headers) {
486
+ if (!Array.isArray(headers)) {
487
+ throw new TypeError('expected headers to be an array')
488
+ }
489
+ return headers.map(x => Buffer.from(x))
490
+ }
491
+
481
492
  /**
482
493
  * @param {*} buffer
483
494
  * @returns {buffer is Buffer}
@@ -863,6 +874,7 @@ module.exports = {
863
874
  removeAllListeners,
864
875
  errorRequest,
865
876
  parseRawHeaders,
877
+ encodeRawHeaders,
866
878
  parseHeaders,
867
879
  parseKeepAliveTimeout,
868
880
  destroy,
@@ -885,6 +897,6 @@ module.exports = {
885
897
  isHttpOrHttpsPrefixed,
886
898
  nodeMajor,
887
899
  nodeMinor,
888
- safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
900
+ safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']),
889
901
  wrapRequestBody
890
902
  }
@@ -49,13 +49,13 @@ const {
49
49
  kMaxResponseSize,
50
50
  kOnError,
51
51
  kResume,
52
- kHTTPContext
52
+ kHTTPContext,
53
+ kClosed
53
54
  } = require('../core/symbols.js')
54
55
 
55
56
  const constants = require('../llhttp/constants.js')
56
57
  const EMPTY_BUF = Buffer.alloc(0)
57
58
  const FastBuffer = Buffer[Symbol.species]
58
- const addListener = util.addListener
59
59
  const removeAllListeners = util.removeAllListeners
60
60
 
61
61
  let extractBody
@@ -779,87 +779,13 @@ async function connectH1 (client, socket) {
779
779
  socket[kBlocking] = false
780
780
  socket[kParser] = new Parser(client, socket, llhttpInstance)
781
781
 
782
- addListener(socket, 'error', function (err) {
783
- assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
782
+ util.addListener(socket, 'error', onHttpSocketError)
783
+ util.addListener(socket, 'readable', onHttpSocketReadable)
784
+ util.addListener(socket, 'end', onHttpSocketEnd)
785
+ util.addListener(socket, 'close', onHttpSocketClose)
784
786
 
785
- const parser = this[kParser]
786
-
787
- // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
788
- // to the user.
789
- if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
790
- // We treat all incoming data so for as a valid response.
791
- parser.onMessageComplete()
792
- return
793
- }
794
-
795
- this[kError] = err
796
-
797
- this[kClient][kOnError](err)
798
- })
799
- addListener(socket, 'readable', function () {
800
- this[kParser]?.readMore()
801
- })
802
- addListener(socket, 'end', function () {
803
- const parser = this[kParser]
804
-
805
- if (parser.statusCode && !parser.shouldKeepAlive) {
806
- // We treat all incoming data so far as a valid response.
807
- parser.onMessageComplete()
808
- return
809
- }
810
-
811
- util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
812
- })
813
- addListener(socket, 'close', function () {
814
- const parser = this[kParser]
815
-
816
- if (parser) {
817
- if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
818
- // We treat all incoming data so far as a valid response.
819
- parser.onMessageComplete()
820
- }
821
-
822
- this[kParser].destroy()
823
- this[kParser] = null
824
- }
825
-
826
- const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
827
-
828
- const client = this[kClient]
829
-
830
- client[kSocket] = null
831
- client[kHTTPContext] = null // TODO (fix): This is hacky...
832
-
833
- if (client.destroyed) {
834
- assert(client[kPending] === 0)
835
-
836
- // Fail entire queue.
837
- const requests = client[kQueue].splice(client[kRunningIdx])
838
- for (let i = 0; i < requests.length; i++) {
839
- const request = requests[i]
840
- util.errorRequest(client, request, err)
841
- }
842
- } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
843
- // Fail head of pipeline.
844
- const request = client[kQueue][client[kRunningIdx]]
845
- client[kQueue][client[kRunningIdx]++] = null
846
-
847
- util.errorRequest(client, request, err)
848
- }
849
-
850
- client[kPendingIdx] = client[kRunningIdx]
851
-
852
- assert(client[kRunning] === 0)
853
-
854
- client.emit('disconnect', client[kUrl], [client], err)
855
-
856
- client[kResume]()
857
- })
858
-
859
- let closed = false
860
- socket.on('close', () => {
861
- closed = true
862
- })
787
+ socket[kClosed] = false
788
+ socket.on('close', onSocketClose)
863
789
 
864
790
  return {
865
791
  version: 'h1',
@@ -875,7 +801,7 @@ async function connectH1 (client, socket) {
875
801
  * @param {() => void} callback
876
802
  */
877
803
  destroy (err, callback) {
878
- if (closed) {
804
+ if (socket[kClosed]) {
879
805
  queueMicrotask(callback)
880
806
  } else {
881
807
  socket.on('close', callback)
@@ -931,6 +857,90 @@ async function connectH1 (client, socket) {
931
857
  }
932
858
  }
933
859
 
860
+ function onHttpSocketError (err) {
861
+ assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
862
+
863
+ const parser = this[kParser]
864
+
865
+ // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded
866
+ // to the user.
867
+ if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) {
868
+ // We treat all incoming data so for as a valid response.
869
+ parser.onMessageComplete()
870
+ return
871
+ }
872
+
873
+ this[kError] = err
874
+
875
+ this[kClient][kOnError](err)
876
+ }
877
+
878
+ function onHttpSocketReadable () {
879
+ this[kParser]?.readMore()
880
+ }
881
+
882
+ function onHttpSocketEnd () {
883
+ const parser = this[kParser]
884
+
885
+ if (parser.statusCode && !parser.shouldKeepAlive) {
886
+ // We treat all incoming data so far as a valid response.
887
+ parser.onMessageComplete()
888
+ return
889
+ }
890
+
891
+ util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
892
+ }
893
+
894
+ function onHttpSocketClose () {
895
+ const parser = this[kParser]
896
+
897
+ if (parser) {
898
+ if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) {
899
+ // We treat all incoming data so far as a valid response.
900
+ parser.onMessageComplete()
901
+ }
902
+
903
+ this[kParser].destroy()
904
+ this[kParser] = null
905
+ }
906
+
907
+ const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
908
+
909
+ const client = this[kClient]
910
+
911
+ client[kSocket] = null
912
+ client[kHTTPContext] = null // TODO (fix): This is hacky...
913
+
914
+ if (client.destroyed) {
915
+ assert(client[kPending] === 0)
916
+
917
+ // Fail entire queue.
918
+ const requests = client[kQueue].splice(client[kRunningIdx])
919
+ for (let i = 0; i < requests.length; i++) {
920
+ const request = requests[i]
921
+ util.errorRequest(client, request, err)
922
+ }
923
+ } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
924
+ // Fail head of pipeline.
925
+ const request = client[kQueue][client[kRunningIdx]]
926
+ client[kQueue][client[kRunningIdx]++] = null
927
+
928
+ util.errorRequest(client, request, err)
929
+ }
930
+
931
+ client[kPendingIdx] = client[kRunningIdx]
932
+
933
+ assert(client[kRunning] === 0)
934
+
935
+ client.emit('disconnect', client[kUrl], [client], err)
936
+
937
+ client[kResume]()
938
+ }
939
+
940
+ function onSocketClose () {
941
+ this[kClosed] = true
942
+ }
943
+
934
944
  /**
935
945
  * @param {import('./client.js')} client
936
946
  */
@@ -991,7 +1001,10 @@ function writeH1 (client, request) {
991
1001
  const expectsPayload = (
992
1002
  method === 'PUT' ||
993
1003
  method === 'POST' ||
994
- method === 'PATCH'
1004
+ method === 'PATCH' ||
1005
+ method === 'QUERY' ||
1006
+ method === 'PROPFIND' ||
1007
+ method === 'PROPPATCH'
995
1008
  )
996
1009
 
997
1010
  if (util.isFormDataLike(body)) {
@@ -1319,7 +1332,7 @@ function writeBuffer (abort, body, client, request, socket, contentLength, heade
1319
1332
  socket.uncork()
1320
1333
  request.onBodySent(body)
1321
1334
 
1322
- if (!expectsPayload) {
1335
+ if (!expectsPayload && request.reset !== false) {
1323
1336
  socket[kReset] = true
1324
1337
  }
1325
1338
  }
@@ -1360,7 +1373,7 @@ async function writeBlob (abort, body, client, request, socket, contentLength, h
1360
1373
  request.onBodySent(buffer)
1361
1374
  request.onRequestSent()
1362
1375
 
1363
- if (!expectsPayload) {
1376
+ if (!expectsPayload && request.reset !== false) {
1364
1377
  socket[kReset] = true
1365
1378
  }
1366
1379
 
@@ -1487,7 +1500,7 @@ class AsyncWriter {
1487
1500
  socket.cork()
1488
1501
 
1489
1502
  if (bytesWritten === 0) {
1490
- if (!expectsPayload) {
1503
+ if (!expectsPayload && request.reset !== false) {
1491
1504
  socket[kReset] = true
1492
1505
  }
1493
1506