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.
@@ -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.
@@ -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 ref of this[kClients].values()) {
87
- const client = ref.deref()
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
- const ref = this[kClients].get(key)
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', this[kOnDrain])
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, new WeakRef(dispatcher))
115
- this[kFinalizer].register(dispatcher, key)
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 ref of this[kClients].values()) {
124
- const client = ref.deref()
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 ref of this[kClients].values()) {
137
- const client = ref.deref()
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
  }
@@ -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, toUSVString } = require('../core/util')
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 = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
168
- const signal = opts && opts.signal
169
-
170
- if (signal) {
171
- try {
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 Promise.resolve(null)
159
+ return null
183
160
  }
184
161
 
185
- return new Promise((resolve, reject) => {
186
- const signalListenerCleanup = signal
187
- ? util.addAbortListener(signal, () => {
188
- this.destroy()
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
- signalListenerCleanup()
195
- if (signal && signal.aborted) {
196
- reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }))
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(toUSVString(Buffer.concat(body)))
294
+ resolve(chunksDecode(body, length))
298
295
  } else if (type === 'json') {
299
- resolve(JSON.parse(Buffer.concat(body)))
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 && key.toString().toLowerCase() === 'keep-alive') {
769
- this.keepAlive += buf.toString()
770
- } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') {
771
- this.connection += buf.toString()
772
- } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') {
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()
@@ -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
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  /** @type {Record<string, string | undefined>} */
2
4
  const headerNameLowerCasedRecord = {}
3
5
 
@@ -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 RequestAbortedError extends UndiciError {
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,
@@ -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
- // tokenRegExp and headerCharRegex have been lifted from
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 (tokenRegExp.exec(method) === null) {
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
- if (
420
- request.host === null &&
421
- key.length === 4 &&
422
- key.toLowerCase() === 'host'
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
- key.length === 17 &&
448
- key.toLowerCase() === 'transfer-encoding'
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++) {