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.
- package/README.md +3 -2
- package/docs/docs/api/BalancedPool.md +1 -1
- package/docs/docs/api/CacheStore.md +100 -0
- package/docs/docs/api/Dispatcher.md +32 -2
- package/docs/docs/api/MockClient.md +1 -1
- package/docs/docs/api/Pool.md +1 -1
- package/docs/docs/api/api-lifecycle.md +2 -2
- package/docs/docs/best-practices/mocking-request.md +2 -2
- package/docs/docs/best-practices/proxy.md +1 -1
- package/index.d.ts +1 -1
- package/index.js +8 -2
- package/lib/api/api-request.js +2 -2
- package/lib/api/readable.js +6 -6
- package/lib/cache/memory-cache-store.js +325 -0
- package/lib/core/connect.js +5 -0
- package/lib/core/constants.js +24 -1
- package/lib/core/request.js +2 -2
- package/lib/core/util.js +13 -1
- package/lib/dispatcher/client-h1.js +100 -87
- package/lib/dispatcher/client-h2.js +168 -96
- package/lib/dispatcher/pool-base.js +3 -3
- package/lib/handler/cache-handler.js +389 -0
- package/lib/handler/cache-revalidation-handler.js +151 -0
- package/lib/handler/redirect-handler.js +5 -3
- package/lib/handler/retry-handler.js +3 -3
- package/lib/interceptor/cache.js +192 -0
- package/lib/interceptor/dns.js +71 -48
- package/lib/util/cache.js +249 -0
- package/lib/web/cache/cache.js +1 -0
- package/lib/web/cache/cachestorage.js +2 -0
- package/lib/web/cookies/index.js +12 -1
- package/lib/web/cookies/parse.js +6 -1
- package/lib/web/eventsource/eventsource.js +2 -0
- package/lib/web/fetch/body.js +1 -5
- package/lib/web/fetch/constants.js +12 -5
- package/lib/web/fetch/data-url.js +2 -2
- package/lib/web/fetch/formdata-parser.js +70 -43
- package/lib/web/fetch/formdata.js +3 -1
- package/lib/web/fetch/headers.js +3 -1
- package/lib/web/fetch/index.js +4 -6
- package/lib/web/fetch/request.js +3 -1
- package/lib/web/fetch/response.js +3 -1
- package/lib/web/fetch/util.js +171 -47
- package/lib/web/fetch/webidl.js +28 -16
- package/lib/web/websocket/constants.js +67 -6
- package/lib/web/websocket/events.js +4 -0
- package/lib/web/websocket/stream/websocketerror.js +1 -1
- package/lib/web/websocket/websocket.js +2 -0
- package/package.json +8 -5
- package/types/cache-interceptor.d.ts +101 -0
- package/types/cookies.d.ts +2 -0
- package/types/dispatcher.d.ts +1 -1
- package/types/fetch.d.ts +9 -8
- package/types/index.d.ts +3 -1
- package/types/interceptors.d.ts +4 -1
- 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
|
package/lib/core/connect.js
CHANGED
|
@@ -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(', ')},`
|
package/lib/core/constants.js
CHANGED
|
@@ -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
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -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
|
|
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',
|
|
783
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|