undici 6.0.1 → 6.2.0
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/docs/api/Dispatcher.md +1 -0
- package/docs/api/Util.md +25 -0
- package/index.js +4 -0
- package/lib/agent.js +35 -32
- package/lib/api/readable.js +42 -48
- package/lib/client.js +10 -5
- package/lib/core/connect.js +1 -1
- package/lib/core/constants.js +2 -0
- package/lib/core/errors.js +10 -20
- package/lib/core/request.js +23 -49
- package/lib/core/tree.js +134 -0
- package/lib/core/util.js +86 -33
- package/lib/fetch/body.js +1 -1
- package/lib/fetch/dataURL.js +49 -44
- package/lib/fetch/headers.js +45 -27
- package/lib/fetch/index.js +96 -82
- package/lib/fetch/response.js +3 -3
- package/lib/fetch/util.js +51 -62
- package/lib/mock/mock-agent.js +6 -17
- package/package.json +2 -1
- package/types/dispatcher.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/types/util.d.ts +31 -0
package/docs/api/Dispatcher.md
CHANGED
|
@@ -209,6 +209,7 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
|
|
|
209
209
|
* **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.
|
|
210
210
|
* **onError** `(error: Error) => void` - Invoked when an error has occurred. May not throw.
|
|
211
211
|
* **onUpgrade** `(statusCode: number, headers: Buffer[], socket: Duplex) => void` (optional) - Invoked when request is upgraded. Required if `DispatchOptions.upgrade` is defined or `DispatchOptions.method === 'CONNECT'`.
|
|
212
|
+
* **onResponseStarted** `() => void` (optional) - Invoked when response is received, before headers have been read.
|
|
212
213
|
* **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
214
|
* **onData** `(chunk: Buffer) => boolean` - Invoked when response payload data is received. Not required for `upgrade` requests.
|
|
214
215
|
* **onComplete** `(trailers: Buffer[]) => void` - Invoked when response payload and trailers have been received and the request has completed. Not required for `upgrade` requests.
|
package/docs/api/Util.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Util
|
|
2
|
+
|
|
3
|
+
Utility API for third-party implementations of the dispatcher API.
|
|
4
|
+
|
|
5
|
+
## `parseHeaders(headers, [obj])`
|
|
6
|
+
|
|
7
|
+
Receives a header object and returns the parsed value.
|
|
8
|
+
|
|
9
|
+
Arguments:
|
|
10
|
+
|
|
11
|
+
- **headers** `Record<string, string | string[]> | (Buffer | string | (Buffer | string)[])[]` (required) - Header object.
|
|
12
|
+
|
|
13
|
+
- **obj** `Record<string, string | string[]>` (optional) - Object to specify a proxy object. The parsed value is assigned to this object. But, if **headers** is an object, it is not used.
|
|
14
|
+
|
|
15
|
+
Returns: `Record<string, string | string[]>` If **headers** is an object, it is **headers**. Otherwise, if **obj** is specified, it is equivalent to **obj**.
|
|
16
|
+
|
|
17
|
+
## `headerNameToString(value)`
|
|
18
|
+
|
|
19
|
+
Retrieves a header name and returns its lowercase value.
|
|
20
|
+
|
|
21
|
+
Arguments:
|
|
22
|
+
|
|
23
|
+
- **value** `string | Buffer` (required) - Header name.
|
|
24
|
+
|
|
25
|
+
Returns: `string`
|
package/index.js
CHANGED
|
@@ -45,6 +45,10 @@ module.exports.createRedirectInterceptor = createRedirectInterceptor
|
|
|
45
45
|
|
|
46
46
|
module.exports.buildConnector = buildConnector
|
|
47
47
|
module.exports.errors = errors
|
|
48
|
+
module.exports.util = {
|
|
49
|
+
parseHeaders: util.parseHeaders,
|
|
50
|
+
headerNameToString: util.headerNameToString
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
function makeDispatcher (fn) {
|
|
50
54
|
return (url, opts, handler) => {
|
package/lib/agent.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { InvalidArgumentError } = require('./core/errors')
|
|
4
|
-
const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
|
|
4
|
+
const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors, kBusy } = require('./core/symbols')
|
|
5
5
|
const DispatcherBase = require('./dispatcher-base')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const Client = require('./client')
|
|
8
8
|
const util = require('./core/util')
|
|
9
9
|
const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
|
|
10
|
-
const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
|
|
11
10
|
|
|
12
11
|
const kOnConnect = Symbol('onConnect')
|
|
13
12
|
const kOnDisconnect = Symbol('onDisconnect')
|
|
@@ -15,8 +14,8 @@ const kOnConnectionError = Symbol('onConnectionError')
|
|
|
15
14
|
const kMaxRedirections = Symbol('maxRedirections')
|
|
16
15
|
const kOnDrain = Symbol('onDrain')
|
|
17
16
|
const kFactory = Symbol('factory')
|
|
18
|
-
const kFinalizer = Symbol('finalizer')
|
|
19
17
|
const kOptions = Symbol('options')
|
|
18
|
+
const kDeleteScheduled = Symbol('deleteScheduled')
|
|
20
19
|
|
|
21
20
|
function defaultFactory (origin, opts) {
|
|
22
21
|
return opts && opts.connections === 1
|
|
@@ -55,12 +54,6 @@ class Agent extends DispatcherBase {
|
|
|
55
54
|
this[kMaxRedirections] = maxRedirections
|
|
56
55
|
this[kFactory] = factory
|
|
57
56
|
this[kClients] = new Map()
|
|
58
|
-
this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => {
|
|
59
|
-
const ref = this[kClients].get(key)
|
|
60
|
-
if (ref !== undefined && ref.deref() === undefined) {
|
|
61
|
-
this[kClients].delete(key)
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
57
|
|
|
65
58
|
const agent = this
|
|
66
59
|
|
|
@@ -83,12 +76,8 @@ class Agent extends DispatcherBase {
|
|
|
83
76
|
|
|
84
77
|
get [kRunning] () {
|
|
85
78
|
let ret = 0
|
|
86
|
-
for (const
|
|
87
|
-
|
|
88
|
-
/* istanbul ignore next: gc is undeterministic */
|
|
89
|
-
if (client) {
|
|
90
|
-
ret += client[kRunning]
|
|
91
|
-
}
|
|
79
|
+
for (const client of this[kClients].values()) {
|
|
80
|
+
ret += client[kRunning]
|
|
92
81
|
}
|
|
93
82
|
return ret
|
|
94
83
|
}
|
|
@@ -101,18 +90,38 @@ class Agent extends DispatcherBase {
|
|
|
101
90
|
throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.')
|
|
102
91
|
}
|
|
103
92
|
|
|
104
|
-
|
|
93
|
+
let dispatcher = this[kClients].get(key)
|
|
105
94
|
|
|
106
|
-
let dispatcher = ref ? ref.deref() : null
|
|
107
95
|
if (!dispatcher) {
|
|
108
96
|
dispatcher = this[kFactory](opts.origin, this[kOptions])
|
|
109
|
-
.on('drain',
|
|
97
|
+
.on('drain', (...args) => {
|
|
98
|
+
this[kOnDrain](...args)
|
|
99
|
+
|
|
100
|
+
// We remove the client if it is not busy for 5 minutes
|
|
101
|
+
// to avoid a long list of clients to saturate memory.
|
|
102
|
+
// Ideally, we could use a FinalizationRegistry here, but
|
|
103
|
+
// it is currently very buggy in Node.js.
|
|
104
|
+
// See
|
|
105
|
+
// * https://github.com/nodejs/node/issues/49344
|
|
106
|
+
// * https://github.com/nodejs/node/issues/47748
|
|
107
|
+
// TODO(mcollina): make the timeout configurable or
|
|
108
|
+
// use an event to remove disconnected clients.
|
|
109
|
+
this[kDeleteScheduled] = setTimeout(() => {
|
|
110
|
+
if (dispatcher[kBusy] === 0) {
|
|
111
|
+
this[kClients].destroy().then(() => {})
|
|
112
|
+
this[kClients].delete(key)
|
|
113
|
+
}
|
|
114
|
+
}, 300_000)
|
|
115
|
+
this[kDeleteScheduled].unref()
|
|
116
|
+
})
|
|
110
117
|
.on('connect', this[kOnConnect])
|
|
111
118
|
.on('disconnect', this[kOnDisconnect])
|
|
112
119
|
.on('connectionError', this[kOnConnectionError])
|
|
113
120
|
|
|
114
|
-
this[kClients].set(key,
|
|
115
|
-
|
|
121
|
+
this[kClients].set(key, dispatcher)
|
|
122
|
+
} else if (dispatcher[kDeleteScheduled]) {
|
|
123
|
+
clearTimeout(dispatcher[kDeleteScheduled])
|
|
124
|
+
dispatcher[kDeleteScheduled] = null
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
return dispatcher.dispatch(opts, handler)
|
|
@@ -120,26 +129,20 @@ class Agent extends DispatcherBase {
|
|
|
120
129
|
|
|
121
130
|
async [kClose] () {
|
|
122
131
|
const closePromises = []
|
|
123
|
-
for (const
|
|
124
|
-
|
|
125
|
-
/* istanbul ignore else: gc is undeterministic */
|
|
126
|
-
if (client) {
|
|
127
|
-
closePromises.push(client.close())
|
|
128
|
-
}
|
|
132
|
+
for (const client of this[kClients].values()) {
|
|
133
|
+
closePromises.push(client.close())
|
|
129
134
|
}
|
|
135
|
+
this[kClients].clear()
|
|
130
136
|
|
|
131
137
|
await Promise.all(closePromises)
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
async [kDestroy] (err) {
|
|
135
141
|
const destroyPromises = []
|
|
136
|
-
for (const
|
|
137
|
-
|
|
138
|
-
/* istanbul ignore else: gc is undeterministic */
|
|
139
|
-
if (client) {
|
|
140
|
-
destroyPromises.push(client.destroy(err))
|
|
141
|
-
}
|
|
142
|
+
for (const client of this[kClients].values()) {
|
|
143
|
+
destroyPromises.push(client.destroy(err))
|
|
142
144
|
}
|
|
145
|
+
this[kClients].clear()
|
|
143
146
|
|
|
144
147
|
await Promise.all(destroyPromises)
|
|
145
148
|
}
|
package/lib/api/readable.js
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
const assert = require('assert')
|
|
6
6
|
const { Readable } = require('stream')
|
|
7
|
-
const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors')
|
|
7
|
+
const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require('../core/errors')
|
|
8
8
|
const util = require('../core/util')
|
|
9
|
-
const { ReadableStreamFrom
|
|
10
|
-
|
|
11
|
-
let Blob
|
|
9
|
+
const { ReadableStreamFrom } = require('../core/util')
|
|
12
10
|
|
|
13
11
|
const kConsume = Symbol('kConsume')
|
|
14
12
|
const kReading = Symbol('kReading')
|
|
@@ -46,11 +44,6 @@ module.exports = class BodyReadable extends Readable {
|
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
destroy (err) {
|
|
49
|
-
if (this.destroyed) {
|
|
50
|
-
// Node < 16
|
|
51
|
-
return this
|
|
52
|
-
}
|
|
53
|
-
|
|
54
47
|
if (!err && !this._readableState.endEmitted) {
|
|
55
48
|
err = new RequestAbortedError()
|
|
56
49
|
}
|
|
@@ -74,17 +67,6 @@ module.exports = class BodyReadable extends Readable {
|
|
|
74
67
|
})
|
|
75
68
|
}
|
|
76
69
|
|
|
77
|
-
emit (ev, ...args) {
|
|
78
|
-
if (ev === 'data') {
|
|
79
|
-
// Node < 16.7
|
|
80
|
-
this._readableState.dataEmitted = true
|
|
81
|
-
} else if (ev === 'error') {
|
|
82
|
-
// Node < 16
|
|
83
|
-
this._readableState.errorEmitted = true
|
|
84
|
-
}
|
|
85
|
-
return super.emit(ev, ...args)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
70
|
on (ev, ...args) {
|
|
89
71
|
if (ev === 'data' || ev === 'readable') {
|
|
90
72
|
this[kReading] = true
|
|
@@ -163,37 +145,31 @@ module.exports = class BodyReadable extends Readable {
|
|
|
163
145
|
return this[kBody]
|
|
164
146
|
}
|
|
165
147
|
|
|
166
|
-
dump (opts) {
|
|
167
|
-
let limit =
|
|
168
|
-
const signal = opts
|
|
169
|
-
|
|
170
|
-
if (signal) {
|
|
171
|
-
|
|
172
|
-
if (typeof signal !== 'object' || !('aborted' in signal)) {
|
|
173
|
-
throw new InvalidArgumentError('signal must be an AbortSignal')
|
|
174
|
-
}
|
|
175
|
-
util.throwIfAborted(signal)
|
|
176
|
-
} catch (err) {
|
|
177
|
-
return Promise.reject(err)
|
|
178
|
-
}
|
|
148
|
+
async dump (opts) {
|
|
149
|
+
let limit = Number.isFinite(opts?.limit) ? opts.limit : 262144
|
|
150
|
+
const signal = opts?.signal
|
|
151
|
+
|
|
152
|
+
if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
|
|
153
|
+
throw new InvalidArgumentError('signal must be an AbortSignal')
|
|
179
154
|
}
|
|
180
155
|
|
|
156
|
+
signal?.throwIfAborted()
|
|
157
|
+
|
|
181
158
|
if (this._readableState.closeEmitted) {
|
|
182
|
-
return
|
|
159
|
+
return null
|
|
183
160
|
}
|
|
184
161
|
|
|
185
|
-
return new Promise((resolve, reject) => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
: noop
|
|
162
|
+
return await new Promise((resolve, reject) => {
|
|
163
|
+
const onAbort = () => {
|
|
164
|
+
this.destroy(signal.reason ?? new AbortError())
|
|
165
|
+
}
|
|
166
|
+
signal?.addEventListener('abort', onAbort)
|
|
191
167
|
|
|
192
168
|
this
|
|
193
169
|
.on('close', function () {
|
|
194
|
-
|
|
195
|
-
if (signal
|
|
196
|
-
reject(signal.reason
|
|
170
|
+
signal?.removeEventListener('abort', onAbort)
|
|
171
|
+
if (signal?.aborted) {
|
|
172
|
+
reject(signal.reason ?? new AbortError())
|
|
197
173
|
} else {
|
|
198
174
|
resolve(null)
|
|
199
175
|
}
|
|
@@ -289,14 +265,35 @@ function consumeStart (consume) {
|
|
|
289
265
|
}
|
|
290
266
|
}
|
|
291
267
|
|
|
268
|
+
/**
|
|
269
|
+
* @param {Buffer[]} chunks
|
|
270
|
+
* @param {number} length
|
|
271
|
+
*/
|
|
272
|
+
function chunksDecode (chunks, length) {
|
|
273
|
+
if (chunks.length === 0 || length === 0) {
|
|
274
|
+
return ''
|
|
275
|
+
}
|
|
276
|
+
const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length)
|
|
277
|
+
|
|
278
|
+
const start =
|
|
279
|
+
buffer.length >= 3 &&
|
|
280
|
+
// Skip BOM.
|
|
281
|
+
buffer[0] === 0xef &&
|
|
282
|
+
buffer[1] === 0xbb &&
|
|
283
|
+
buffer[2] === 0xbf
|
|
284
|
+
? 3
|
|
285
|
+
: 0
|
|
286
|
+
return buffer.utf8Slice(start, buffer.length - start)
|
|
287
|
+
}
|
|
288
|
+
|
|
292
289
|
function consumeEnd (consume) {
|
|
293
290
|
const { type, body, resolve, stream, length } = consume
|
|
294
291
|
|
|
295
292
|
try {
|
|
296
293
|
if (type === 'text') {
|
|
297
|
-
resolve(
|
|
294
|
+
resolve(chunksDecode(body, length))
|
|
298
295
|
} else if (type === 'json') {
|
|
299
|
-
resolve(JSON.parse(
|
|
296
|
+
resolve(JSON.parse(chunksDecode(body, length)))
|
|
300
297
|
} else if (type === 'arrayBuffer') {
|
|
301
298
|
const dst = new Uint8Array(length)
|
|
302
299
|
|
|
@@ -308,9 +305,6 @@ function consumeEnd (consume) {
|
|
|
308
305
|
|
|
309
306
|
resolve(dst.buffer)
|
|
310
307
|
} else if (type === 'blob') {
|
|
311
|
-
if (!Blob) {
|
|
312
|
-
Blob = require('buffer').Blob
|
|
313
|
-
}
|
|
314
308
|
resolve(new Blob(body, { type: stream[kContentType] }))
|
|
315
309
|
}
|
|
316
310
|
|
package/lib/client.js
CHANGED
|
@@ -740,6 +740,7 @@ class Parser {
|
|
|
740
740
|
if (!request) {
|
|
741
741
|
return -1
|
|
742
742
|
}
|
|
743
|
+
request.onResponseStarted()
|
|
743
744
|
}
|
|
744
745
|
|
|
745
746
|
onHeaderField (buf) {
|
|
@@ -765,11 +766,14 @@ class Parser {
|
|
|
765
766
|
}
|
|
766
767
|
|
|
767
768
|
const key = this.headers[len - 2]
|
|
768
|
-
if (key.length === 10
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
769
|
+
if (key.length === 10) {
|
|
770
|
+
const headerName = util.bufferToLowerCasedHeaderName(key)
|
|
771
|
+
if (headerName === 'keep-alive') {
|
|
772
|
+
this.keepAlive += buf.toString()
|
|
773
|
+
} else if (headerName === 'connection') {
|
|
774
|
+
this.connection += buf.toString()
|
|
775
|
+
}
|
|
776
|
+
} else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === 'content-length') {
|
|
773
777
|
this.contentLength += buf.toString()
|
|
774
778
|
}
|
|
775
779
|
|
|
@@ -1783,6 +1787,7 @@ function writeH2 (client, session, request) {
|
|
|
1783
1787
|
|
|
1784
1788
|
stream.once('response', headers => {
|
|
1785
1789
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
1790
|
+
request.onResponseStarted()
|
|
1786
1791
|
|
|
1787
1792
|
if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
|
|
1788
1793
|
stream.pause()
|
package/lib/core/connect.js
CHANGED
|
@@ -15,7 +15,7 @@ let tls // include tls conditionally since it is not always available
|
|
|
15
15
|
let SessionCache
|
|
16
16
|
// FIXME: remove workaround when the Node bug is fixed
|
|
17
17
|
// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
|
|
18
|
-
if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) {
|
|
18
|
+
if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
|
|
19
19
|
SessionCache = class WeakSessionCache {
|
|
20
20
|
constructor (maxCachedSessions) {
|
|
21
21
|
this._maxCachedSessions = maxCachedSessions
|
package/lib/core/constants.js
CHANGED
package/lib/core/errors.js
CHANGED
|
@@ -11,7 +11,6 @@ class UndiciError extends Error {
|
|
|
11
11
|
class ConnectTimeoutError extends UndiciError {
|
|
12
12
|
constructor (message) {
|
|
13
13
|
super(message)
|
|
14
|
-
Error.captureStackTrace(this, ConnectTimeoutError)
|
|
15
14
|
this.name = 'ConnectTimeoutError'
|
|
16
15
|
this.message = message || 'Connect Timeout Error'
|
|
17
16
|
this.code = 'UND_ERR_CONNECT_TIMEOUT'
|
|
@@ -21,7 +20,6 @@ class ConnectTimeoutError extends UndiciError {
|
|
|
21
20
|
class HeadersTimeoutError extends UndiciError {
|
|
22
21
|
constructor (message) {
|
|
23
22
|
super(message)
|
|
24
|
-
Error.captureStackTrace(this, HeadersTimeoutError)
|
|
25
23
|
this.name = 'HeadersTimeoutError'
|
|
26
24
|
this.message = message || 'Headers Timeout Error'
|
|
27
25
|
this.code = 'UND_ERR_HEADERS_TIMEOUT'
|
|
@@ -31,7 +29,6 @@ class HeadersTimeoutError extends UndiciError {
|
|
|
31
29
|
class HeadersOverflowError extends UndiciError {
|
|
32
30
|
constructor (message) {
|
|
33
31
|
super(message)
|
|
34
|
-
Error.captureStackTrace(this, HeadersOverflowError)
|
|
35
32
|
this.name = 'HeadersOverflowError'
|
|
36
33
|
this.message = message || 'Headers Overflow Error'
|
|
37
34
|
this.code = 'UND_ERR_HEADERS_OVERFLOW'
|
|
@@ -41,7 +38,6 @@ class HeadersOverflowError extends UndiciError {
|
|
|
41
38
|
class BodyTimeoutError extends UndiciError {
|
|
42
39
|
constructor (message) {
|
|
43
40
|
super(message)
|
|
44
|
-
Error.captureStackTrace(this, BodyTimeoutError)
|
|
45
41
|
this.name = 'BodyTimeoutError'
|
|
46
42
|
this.message = message || 'Body Timeout Error'
|
|
47
43
|
this.code = 'UND_ERR_BODY_TIMEOUT'
|
|
@@ -51,7 +47,6 @@ class BodyTimeoutError extends UndiciError {
|
|
|
51
47
|
class ResponseStatusCodeError extends UndiciError {
|
|
52
48
|
constructor (message, statusCode, headers, body) {
|
|
53
49
|
super(message)
|
|
54
|
-
Error.captureStackTrace(this, ResponseStatusCodeError)
|
|
55
50
|
this.name = 'ResponseStatusCodeError'
|
|
56
51
|
this.message = message || 'Response Status Code Error'
|
|
57
52
|
this.code = 'UND_ERR_RESPONSE_STATUS_CODE'
|
|
@@ -65,7 +60,6 @@ class ResponseStatusCodeError extends UndiciError {
|
|
|
65
60
|
class InvalidArgumentError extends UndiciError {
|
|
66
61
|
constructor (message) {
|
|
67
62
|
super(message)
|
|
68
|
-
Error.captureStackTrace(this, InvalidArgumentError)
|
|
69
63
|
this.name = 'InvalidArgumentError'
|
|
70
64
|
this.message = message || 'Invalid Argument Error'
|
|
71
65
|
this.code = 'UND_ERR_INVALID_ARG'
|
|
@@ -75,17 +69,23 @@ class InvalidArgumentError extends UndiciError {
|
|
|
75
69
|
class InvalidReturnValueError extends UndiciError {
|
|
76
70
|
constructor (message) {
|
|
77
71
|
super(message)
|
|
78
|
-
Error.captureStackTrace(this, InvalidReturnValueError)
|
|
79
72
|
this.name = 'InvalidReturnValueError'
|
|
80
73
|
this.message = message || 'Invalid Return Value Error'
|
|
81
74
|
this.code = 'UND_ERR_INVALID_RETURN_VALUE'
|
|
82
75
|
}
|
|
83
76
|
}
|
|
84
77
|
|
|
85
|
-
class
|
|
78
|
+
class AbortError extends UndiciError {
|
|
79
|
+
constructor (message) {
|
|
80
|
+
super(message)
|
|
81
|
+
this.name = 'AbortError'
|
|
82
|
+
this.message = message || 'The operation was aborted'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class RequestAbortedError extends AbortError {
|
|
86
87
|
constructor (message) {
|
|
87
88
|
super(message)
|
|
88
|
-
Error.captureStackTrace(this, RequestAbortedError)
|
|
89
89
|
this.name = 'AbortError'
|
|
90
90
|
this.message = message || 'Request aborted'
|
|
91
91
|
this.code = 'UND_ERR_ABORTED'
|
|
@@ -95,7 +95,6 @@ class RequestAbortedError extends UndiciError {
|
|
|
95
95
|
class InformationalError extends UndiciError {
|
|
96
96
|
constructor (message) {
|
|
97
97
|
super(message)
|
|
98
|
-
Error.captureStackTrace(this, InformationalError)
|
|
99
98
|
this.name = 'InformationalError'
|
|
100
99
|
this.message = message || 'Request information'
|
|
101
100
|
this.code = 'UND_ERR_INFO'
|
|
@@ -105,7 +104,6 @@ class InformationalError extends UndiciError {
|
|
|
105
104
|
class RequestContentLengthMismatchError extends UndiciError {
|
|
106
105
|
constructor (message) {
|
|
107
106
|
super(message)
|
|
108
|
-
Error.captureStackTrace(this, RequestContentLengthMismatchError)
|
|
109
107
|
this.name = 'RequestContentLengthMismatchError'
|
|
110
108
|
this.message = message || 'Request body length does not match content-length header'
|
|
111
109
|
this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'
|
|
@@ -115,7 +113,6 @@ class RequestContentLengthMismatchError extends UndiciError {
|
|
|
115
113
|
class ResponseContentLengthMismatchError extends UndiciError {
|
|
116
114
|
constructor (message) {
|
|
117
115
|
super(message)
|
|
118
|
-
Error.captureStackTrace(this, ResponseContentLengthMismatchError)
|
|
119
116
|
this.name = 'ResponseContentLengthMismatchError'
|
|
120
117
|
this.message = message || 'Response body length does not match content-length header'
|
|
121
118
|
this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'
|
|
@@ -125,7 +122,6 @@ class ResponseContentLengthMismatchError extends UndiciError {
|
|
|
125
122
|
class ClientDestroyedError extends UndiciError {
|
|
126
123
|
constructor (message) {
|
|
127
124
|
super(message)
|
|
128
|
-
Error.captureStackTrace(this, ClientDestroyedError)
|
|
129
125
|
this.name = 'ClientDestroyedError'
|
|
130
126
|
this.message = message || 'The client is destroyed'
|
|
131
127
|
this.code = 'UND_ERR_DESTROYED'
|
|
@@ -135,7 +131,6 @@ class ClientDestroyedError extends UndiciError {
|
|
|
135
131
|
class ClientClosedError extends UndiciError {
|
|
136
132
|
constructor (message) {
|
|
137
133
|
super(message)
|
|
138
|
-
Error.captureStackTrace(this, ClientClosedError)
|
|
139
134
|
this.name = 'ClientClosedError'
|
|
140
135
|
this.message = message || 'The client is closed'
|
|
141
136
|
this.code = 'UND_ERR_CLOSED'
|
|
@@ -145,7 +140,6 @@ class ClientClosedError extends UndiciError {
|
|
|
145
140
|
class SocketError extends UndiciError {
|
|
146
141
|
constructor (message, socket) {
|
|
147
142
|
super(message)
|
|
148
|
-
Error.captureStackTrace(this, SocketError)
|
|
149
143
|
this.name = 'SocketError'
|
|
150
144
|
this.message = message || 'Socket error'
|
|
151
145
|
this.code = 'UND_ERR_SOCKET'
|
|
@@ -156,7 +150,6 @@ class SocketError extends UndiciError {
|
|
|
156
150
|
class NotSupportedError extends UndiciError {
|
|
157
151
|
constructor (message) {
|
|
158
152
|
super(message)
|
|
159
|
-
Error.captureStackTrace(this, NotSupportedError)
|
|
160
153
|
this.name = 'NotSupportedError'
|
|
161
154
|
this.message = message || 'Not supported error'
|
|
162
155
|
this.code = 'UND_ERR_NOT_SUPPORTED'
|
|
@@ -166,7 +159,6 @@ class NotSupportedError extends UndiciError {
|
|
|
166
159
|
class BalancedPoolMissingUpstreamError extends UndiciError {
|
|
167
160
|
constructor (message) {
|
|
168
161
|
super(message)
|
|
169
|
-
Error.captureStackTrace(this, NotSupportedError)
|
|
170
162
|
this.name = 'MissingUpstreamError'
|
|
171
163
|
this.message = message || 'No upstream has been added to the BalancedPool'
|
|
172
164
|
this.code = 'UND_ERR_BPL_MISSING_UPSTREAM'
|
|
@@ -176,7 +168,6 @@ class BalancedPoolMissingUpstreamError extends UndiciError {
|
|
|
176
168
|
class HTTPParserError extends Error {
|
|
177
169
|
constructor (message, code, data) {
|
|
178
170
|
super(message)
|
|
179
|
-
Error.captureStackTrace(this, HTTPParserError)
|
|
180
171
|
this.name = 'HTTPParserError'
|
|
181
172
|
this.code = code ? `HPE_${code}` : undefined
|
|
182
173
|
this.data = data ? data.toString() : undefined
|
|
@@ -186,7 +177,6 @@ class HTTPParserError extends Error {
|
|
|
186
177
|
class ResponseExceededMaxSizeError extends UndiciError {
|
|
187
178
|
constructor (message) {
|
|
188
179
|
super(message)
|
|
189
|
-
Error.captureStackTrace(this, ResponseExceededMaxSizeError)
|
|
190
180
|
this.name = 'ResponseExceededMaxSizeError'
|
|
191
181
|
this.message = message || 'Response content exceeded max size'
|
|
192
182
|
this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE'
|
|
@@ -196,7 +186,6 @@ class ResponseExceededMaxSizeError extends UndiciError {
|
|
|
196
186
|
class RequestRetryError extends UndiciError {
|
|
197
187
|
constructor (message, code, { headers, data }) {
|
|
198
188
|
super(message)
|
|
199
|
-
Error.captureStackTrace(this, RequestRetryError)
|
|
200
189
|
this.name = 'RequestRetryError'
|
|
201
190
|
this.message = message || 'Request retry error'
|
|
202
191
|
this.code = 'UND_ERR_REQ_RETRY'
|
|
@@ -207,6 +196,7 @@ class RequestRetryError extends UndiciError {
|
|
|
207
196
|
}
|
|
208
197
|
|
|
209
198
|
module.exports = {
|
|
199
|
+
AbortError,
|
|
210
200
|
HTTPParserError,
|
|
211
201
|
UndiciError,
|
|
212
202
|
HeadersTimeoutError,
|
package/lib/core/request.js
CHANGED
|
@@ -7,17 +7,11 @@ const {
|
|
|
7
7
|
const assert = require('assert')
|
|
8
8
|
const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
|
|
9
9
|
const util = require('./util')
|
|
10
|
+
const { headerNameLowerCasedRecord } = require('./constants')
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
+
// headerCharRegex have been lifted from
|
|
12
13
|
// https://github.com/nodejs/node/blob/main/lib/_http_common.js
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
* Verifies that the given val is a valid HTTP token
|
|
16
|
-
* per the rules defined in RFC 7230
|
|
17
|
-
* See https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
18
|
-
*/
|
|
19
|
-
const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
|
|
20
|
-
|
|
21
15
|
/**
|
|
22
16
|
* Matches if val contains an invalid field-vchar
|
|
23
17
|
* field-value = *( field-content / obs-fold )
|
|
@@ -80,7 +74,7 @@ class Request {
|
|
|
80
74
|
|
|
81
75
|
if (typeof method !== 'string') {
|
|
82
76
|
throw new InvalidArgumentError('method must be a string')
|
|
83
|
-
} else if (
|
|
77
|
+
} else if (!util.isValidHTTPToken(method)) {
|
|
84
78
|
throw new InvalidArgumentError('invalid request method')
|
|
85
79
|
}
|
|
86
80
|
|
|
@@ -259,6 +253,10 @@ class Request {
|
|
|
259
253
|
}
|
|
260
254
|
}
|
|
261
255
|
|
|
256
|
+
onResponseStarted () {
|
|
257
|
+
return this[kHandler].onResponseStarted?.()
|
|
258
|
+
}
|
|
259
|
+
|
|
262
260
|
onHeaders (statusCode, headers, resume, statusText) {
|
|
263
261
|
assert(!this.aborted)
|
|
264
262
|
assert(!this.completed)
|
|
@@ -416,65 +414,41 @@ function processHeader (request, key, val, skipAppend = false) {
|
|
|
416
414
|
return
|
|
417
415
|
}
|
|
418
416
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
key.toLowerCase()
|
|
423
|
-
|
|
417
|
+
let headerName = headerNameLowerCasedRecord[key]
|
|
418
|
+
|
|
419
|
+
if (headerName === undefined) {
|
|
420
|
+
headerName = key.toLowerCase()
|
|
421
|
+
if (headerNameLowerCasedRecord[headerName] === undefined && !util.isValidHTTPToken(headerName)) {
|
|
422
|
+
throw new InvalidArgumentError('invalid header key')
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (request.host === null && headerName === 'host') {
|
|
424
427
|
if (headerCharRegex.exec(val) !== null) {
|
|
425
428
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
426
429
|
}
|
|
427
430
|
// Consumed by Client
|
|
428
431
|
request.host = val
|
|
429
|
-
} else if (
|
|
430
|
-
request.contentLength === null &&
|
|
431
|
-
key.length === 14 &&
|
|
432
|
-
key.toLowerCase() === 'content-length'
|
|
433
|
-
) {
|
|
432
|
+
} else if (request.contentLength === null && headerName === 'content-length') {
|
|
434
433
|
request.contentLength = parseInt(val, 10)
|
|
435
434
|
if (!Number.isFinite(request.contentLength)) {
|
|
436
435
|
throw new InvalidArgumentError('invalid content-length header')
|
|
437
436
|
}
|
|
438
|
-
} else if (
|
|
439
|
-
request.contentType === null &&
|
|
440
|
-
key.length === 12 &&
|
|
441
|
-
key.toLowerCase() === 'content-type'
|
|
442
|
-
) {
|
|
437
|
+
} else if (request.contentType === null && headerName === 'content-type') {
|
|
443
438
|
request.contentType = val
|
|
444
439
|
if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
|
|
445
440
|
else request.headers += processHeaderValue(key, val)
|
|
446
|
-
} else if (
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
) {
|
|
450
|
-
throw new InvalidArgumentError('invalid transfer-encoding header')
|
|
451
|
-
} else if (
|
|
452
|
-
key.length === 10 &&
|
|
453
|
-
key.toLowerCase() === 'connection'
|
|
454
|
-
) {
|
|
441
|
+
} else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
|
|
442
|
+
throw new InvalidArgumentError(`invalid ${headerName} header`)
|
|
443
|
+
} else if (headerName === 'connection') {
|
|
455
444
|
const value = typeof val === 'string' ? val.toLowerCase() : null
|
|
456
445
|
if (value !== 'close' && value !== 'keep-alive') {
|
|
457
446
|
throw new InvalidArgumentError('invalid connection header')
|
|
458
447
|
} else if (value === 'close') {
|
|
459
448
|
request.reset = true
|
|
460
449
|
}
|
|
461
|
-
} else if (
|
|
462
|
-
key.length === 10 &&
|
|
463
|
-
key.toLowerCase() === 'keep-alive'
|
|
464
|
-
) {
|
|
465
|
-
throw new InvalidArgumentError('invalid keep-alive header')
|
|
466
|
-
} else if (
|
|
467
|
-
key.length === 7 &&
|
|
468
|
-
key.toLowerCase() === 'upgrade'
|
|
469
|
-
) {
|
|
470
|
-
throw new InvalidArgumentError('invalid upgrade header')
|
|
471
|
-
} else if (
|
|
472
|
-
key.length === 6 &&
|
|
473
|
-
key.toLowerCase() === 'expect'
|
|
474
|
-
) {
|
|
450
|
+
} else if (headerName === 'expect') {
|
|
475
451
|
throw new NotSupportedError('expect header not supported')
|
|
476
|
-
} else if (tokenRegExp.exec(key) === null) {
|
|
477
|
-
throw new InvalidArgumentError('invalid header key')
|
|
478
452
|
} else {
|
|
479
453
|
if (Array.isArray(val)) {
|
|
480
454
|
for (let i = 0; i < val.length; i++) {
|