undici 6.0.0 → 6.1.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
@@ -124,6 +124,7 @@ class Agent extends DispatcherBase {
124
124
  const client = ref.deref()
125
125
  /* istanbul ignore else: gc is undeterministic */
126
126
  if (client) {
127
+ this[kFinalizer].unregister(client)
127
128
  closePromises.push(client.close())
128
129
  }
129
130
  }
@@ -137,6 +138,7 @@ class Agent extends DispatcherBase {
137
138
  const client = ref.deref()
138
139
  /* istanbul ignore else: gc is undeterministic */
139
140
  if (client) {
141
+ this[kFinalizer].unregister(client)
140
142
  destroyPromises.push(client.destroy(err))
141
143
  }
142
144
  }
@@ -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
  }
@@ -62,15 +55,16 @@ module.exports = class BodyReadable extends Readable {
62
55
  return super.destroy(err)
63
56
  }
64
57
 
65
- emit (ev, ...args) {
66
- if (ev === 'data') {
67
- // Node < 16.7
68
- this._readableState.dataEmitted = true
69
- } else if (ev === 'error') {
70
- // Node < 16
71
- this._readableState.errorEmitted = true
72
- }
73
- return super.emit(ev, ...args)
58
+ _destroy (err, callback) {
59
+ // Workaround for Node "bug". If the stream is destroyed in same
60
+ // tick as it is created, then a user who is waiting for a
61
+ // promise (i.e micro tick) for installing a 'error' listener will
62
+ // never get a chance and will always encounter an unhandled exception.
63
+ // - tick => process.nextTick(fn)
64
+ // - micro tick => queueMicrotask(fn)
65
+ queueMicrotask(() => {
66
+ callback(err)
67
+ })
74
68
  }
75
69
 
76
70
  on (ev, ...args) {
@@ -151,37 +145,31 @@ module.exports = class BodyReadable extends Readable {
151
145
  return this[kBody]
152
146
  }
153
147
 
154
- dump (opts) {
155
- let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
156
- const signal = opts && opts.signal
157
-
158
- if (signal) {
159
- try {
160
- if (typeof signal !== 'object' || !('aborted' in signal)) {
161
- throw new InvalidArgumentError('signal must be an AbortSignal')
162
- }
163
- util.throwIfAborted(signal)
164
- } catch (err) {
165
- return Promise.reject(err)
166
- }
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')
167
154
  }
168
155
 
169
- if (this.closed) {
170
- return Promise.resolve(null)
156
+ signal?.throwIfAborted()
157
+
158
+ if (this._readableState.closeEmitted) {
159
+ return null
171
160
  }
172
161
 
173
- return new Promise((resolve, reject) => {
174
- const signalListenerCleanup = signal
175
- ? util.addAbortListener(signal, () => {
176
- this.destroy()
177
- })
178
- : noop
162
+ return await new Promise((resolve, reject) => {
163
+ const onAbort = () => {
164
+ this.destroy(signal.reason ?? new AbortError())
165
+ }
166
+ signal?.addEventListener('abort', onAbort)
179
167
 
180
168
  this
181
169
  .on('close', function () {
182
- signalListenerCleanup()
183
- if (signal && signal.aborted) {
184
- 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())
185
173
  } else {
186
174
  resolve(null)
187
175
  }
@@ -210,33 +198,44 @@ function isUnusable (self) {
210
198
  }
211
199
 
212
200
  async function consume (stream, type) {
213
- if (isUnusable(stream)) {
214
- throw new TypeError('unusable')
215
- }
216
-
217
201
  assert(!stream[kConsume])
218
202
 
219
203
  return new Promise((resolve, reject) => {
220
- stream[kConsume] = {
221
- type,
222
- stream,
223
- resolve,
224
- reject,
225
- length: 0,
226
- body: []
227
- }
204
+ if (isUnusable(stream)) {
205
+ const rState = stream._readableState
206
+ if (rState.destroyed && rState.closeEmitted === false) {
207
+ stream
208
+ .on('error', err => {
209
+ reject(err)
210
+ })
211
+ .on('close', () => {
212
+ reject(new TypeError('unusable'))
213
+ })
214
+ } else {
215
+ reject(rState.errored ?? new TypeError('unusable'))
216
+ }
217
+ } else {
218
+ stream[kConsume] = {
219
+ type,
220
+ stream,
221
+ resolve,
222
+ reject,
223
+ length: 0,
224
+ body: []
225
+ }
226
+
227
+ stream
228
+ .on('error', function (err) {
229
+ consumeFinish(this[kConsume], err)
230
+ })
231
+ .on('close', function () {
232
+ if (this[kConsume].body !== null) {
233
+ consumeFinish(this[kConsume], new RequestAbortedError())
234
+ }
235
+ })
228
236
 
229
- stream
230
- .on('error', function (err) {
231
- consumeFinish(this[kConsume], err)
232
- })
233
- .on('close', function () {
234
- if (this[kConsume].body !== null) {
235
- consumeFinish(this[kConsume], new RequestAbortedError())
236
- }
237
- })
238
-
239
- process.nextTick(consumeStart, stream[kConsume])
237
+ queueMicrotask(() => consumeStart(stream[kConsume]))
238
+ }
240
239
  })
241
240
  }
242
241
 
@@ -266,14 +265,35 @@ function consumeStart (consume) {
266
265
  }
267
266
  }
268
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
+
269
289
  function consumeEnd (consume) {
270
290
  const { type, body, resolve, stream, length } = consume
271
291
 
272
292
  try {
273
293
  if (type === 'text') {
274
- resolve(toUSVString(Buffer.concat(body)))
294
+ resolve(chunksDecode(body, length))
275
295
  } else if (type === 'json') {
276
- resolve(JSON.parse(Buffer.concat(body)))
296
+ resolve(JSON.parse(chunksDecode(body, length)))
277
297
  } else if (type === 'arrayBuffer') {
278
298
  const dst = new Uint8Array(length)
279
299
 
@@ -285,9 +305,6 @@ function consumeEnd (consume) {
285
305
 
286
306
  resolve(dst.buffer)
287
307
  } else if (type === 'blob') {
288
- if (!Blob) {
289
- Blob = require('buffer').Blob
290
- }
291
308
  resolve(new Blob(body, { type: stream[kContentType] }))
292
309
  }
293
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()
@@ -1963,12 +1968,19 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
1963
1968
  body.resume()
1964
1969
  }
1965
1970
  }
1966
- const onAbort = function () {
1967
- if (finished) {
1968
- return
1971
+ const onClose = function () {
1972
+ // 'close' might be emitted *before* 'error' for
1973
+ // broken streams. Wait a tick to avoid this case.
1974
+ queueMicrotask(() => {
1975
+ // It's only safe to remove 'error' listener after
1976
+ // 'close'.
1977
+ body.removeListener('error', onFinished)
1978
+ })
1979
+
1980
+ if (!finished) {
1981
+ const err = new RequestAbortedError()
1982
+ queueMicrotask(() => onFinished(err))
1969
1983
  }
1970
- const err = new RequestAbortedError()
1971
- queueMicrotask(() => onFinished(err))
1972
1984
  }
1973
1985
  const onFinished = function (err) {
1974
1986
  if (finished) {
@@ -1986,8 +1998,7 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
1986
1998
  body
1987
1999
  .removeListener('data', onData)
1988
2000
  .removeListener('end', onFinished)
1989
- .removeListener('error', onFinished)
1990
- .removeListener('close', onAbort)
2001
+ .removeListener('close', onClose)
1991
2002
 
1992
2003
  if (!err) {
1993
2004
  try {
@@ -2010,7 +2021,7 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
2010
2021
  .on('data', onData)
2011
2022
  .on('end', onFinished)
2012
2023
  .on('error', onFinished)
2013
- .on('close', onAbort)
2024
+ .on('close', onClose)
2014
2025
 
2015
2026
  if (body.resume) {
2016
2027
  body.resume()
@@ -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++) {