undici 6.11.0 → 6.12.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.
@@ -22,7 +22,6 @@ const {
22
22
  kSocket,
23
23
  kStrictContentLength,
24
24
  kOnError,
25
- // HTTP2
26
25
  kMaxConcurrentStreams,
27
26
  kHTTP2Session,
28
27
  kResume
@@ -55,14 +54,20 @@ const {
55
54
  } = http2
56
55
 
57
56
  function parseH2Headers (headers) {
58
- // set-cookie is always an array. Duplicates are added to the array.
59
- // For duplicate cookie headers, the values are joined together with '; '.
60
- headers = Object.entries(headers).flat(2)
61
-
62
57
  const result = []
63
58
 
64
- for (const header of headers) {
65
- result.push(Buffer.from(header))
59
+ for (const [name, value] of Object.entries(headers)) {
60
+ // h2 may concat the header value by array
61
+ // e.g. Set-Cookie
62
+ if (Array.isArray(value)) {
63
+ for (const subvalue of value) {
64
+ // we need to provide each header value of header name
65
+ // because the headers handler expect name-value pair
66
+ result.push(Buffer.from(name), Buffer.from(subvalue))
67
+ }
68
+ } else {
69
+ result.push(Buffer.from(name), Buffer.from(value))
70
+ }
66
71
  }
67
72
 
68
73
  return result
@@ -86,16 +91,18 @@ async function connectH2 (client, socket) {
86
91
  session[kOpenStreams] = 0
87
92
  session[kClient] = client
88
93
  session[kSocket] = socket
89
- session.on('error', onHttp2SessionError)
90
- session.on('frameError', onHttp2FrameError)
91
- session.on('end', onHttp2SessionEnd)
92
- session.on('goaway', onHTTP2GoAway)
93
- session.on('close', function () {
94
+
95
+ util.addListener(session, 'error', onHttp2SessionError)
96
+ util.addListener(session, 'frameError', onHttp2FrameError)
97
+ util.addListener(session, 'end', onHttp2SessionEnd)
98
+ util.addListener(session, 'goaway', onHTTP2GoAway)
99
+ util.addListener(session, 'close', function () {
94
100
  const { [kClient]: client } = this
95
101
 
96
- const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
102
+ const err = this[kSocket][kError] || new SocketError('closed', util.getSocketInfo(this))
97
103
 
98
104
  client[kSocket] = null
105
+ client[kHTTP2Session] = null
99
106
 
100
107
  assert(client[kPending] === 0)
101
108
 
@@ -103,7 +110,7 @@ async function connectH2 (client, socket) {
103
110
  const requests = client[kQueue].splice(client[kRunningIdx])
104
111
  for (let i = 0; i < requests.length; i++) {
105
112
  const request = requests[i]
106
- errorRequest(client, request, err)
113
+ util.errorRequest(client, request, err)
107
114
  }
108
115
 
109
116
  client[kPendingIdx] = client[kRunningIdx]
@@ -114,19 +121,21 @@ async function connectH2 (client, socket) {
114
121
 
115
122
  client[kResume]()
116
123
  })
124
+
117
125
  session.unref()
118
126
 
119
127
  client[kHTTP2Session] = session
120
128
  socket[kHTTP2Session] = session
121
129
 
122
- socket.on('error', function (err) {
130
+ util.addListener(socket, 'error', function (err) {
123
131
  assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
124
132
 
125
133
  this[kError] = err
126
134
 
127
135
  this[kClient][kOnError](err)
128
136
  })
129
- socket.on('end', function () {
137
+
138
+ util.addListener(socket, 'end', function () {
130
139
  util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this)))
131
140
  })
132
141
 
@@ -166,67 +175,42 @@ function onHttp2SessionError (err) {
166
175
  assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
167
176
 
168
177
  this[kSocket][kError] = err
169
-
170
178
  this[kClient][kOnError](err)
171
179
  }
172
180
 
173
181
  function onHttp2FrameError (type, code, id) {
174
- const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
175
-
176
182
  if (id === 0) {
183
+ const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
177
184
  this[kSocket][kError] = err
178
185
  this[kClient][kOnError](err)
179
186
  }
180
187
  }
181
188
 
182
189
  function onHttp2SessionEnd () {
183
- this.destroy(new SocketError('other side closed'))
184
- util.destroy(this[kSocket], new SocketError('other side closed'))
190
+ const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket]))
191
+ this.destroy(err)
192
+ util.destroy(this[kSocket], err)
185
193
  }
186
194
 
195
+ /**
196
+ * This is the root cause of #3011
197
+ * We need to handle GOAWAY frames properly, and trigger the session close
198
+ * along with the socket right away
199
+ * Find a way to trigger the close cycle from here on.
200
+ */
187
201
  function onHTTP2GoAway (code) {
188
- const client = this[kClient]
189
202
  const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`)
190
- client[kSocket] = null
191
- client[kHTTP2Session] = null
192
203
 
193
- if (client.destroyed) {
194
- assert(this[kPending] === 0)
195
-
196
- // Fail entire queue.
197
- const requests = client[kQueue].splice(client[kRunningIdx])
198
- for (let i = 0; i < requests.length; i++) {
199
- const request = requests[i]
200
- errorRequest(this, request, err)
201
- }
202
- } else if (client[kRunning] > 0) {
203
- // Fail head of pipeline.
204
- const request = client[kQueue][client[kRunningIdx]]
205
- client[kQueue][client[kRunningIdx]++] = null
206
-
207
- errorRequest(client, request, err)
208
- }
209
-
210
- client[kPendingIdx] = client[kRunningIdx]
211
-
212
- assert(client[kRunning] === 0)
213
-
214
- client.emit('disconnect',
215
- client[kUrl],
216
- [client],
217
- err
218
- )
219
-
220
- client[kResume]()
221
- }
204
+ // We need to trigger the close cycle right away
205
+ // We need to destroy the session and the socket
206
+ // Requests should be failed with the error after the current one is handled
207
+ this[kSocket][kError] = err
208
+ this[kClient][kOnError](err)
222
209
 
223
- function errorRequest (client, request, err) {
224
- try {
225
- request.onError(err)
226
- assert(request.aborted)
227
- } catch (err) {
228
- client.emit('error', err)
229
- }
210
+ this.unref()
211
+ // We send the GOAWAY frame response as no error
212
+ this.destroy()
213
+ util.destroy(this[kSocket], err)
230
214
  }
231
215
 
232
216
  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
@@ -239,7 +223,7 @@ function writeH2 (client, request) {
239
223
  const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
240
224
 
241
225
  if (upgrade) {
242
- errorRequest(client, request, new Error('Upgrade not supported for H2'))
226
+ util.errorRequest(client, request, new Error('Upgrade not supported for H2'))
243
227
  return false
244
228
  }
245
229
 
@@ -292,10 +276,10 @@ function writeH2 (client, request) {
292
276
  }
293
277
  }
294
278
 
295
- errorRequest(client, request, err)
279
+ util.errorRequest(client, request, err)
296
280
  })
297
281
  } catch (err) {
298
- errorRequest(client, request, err)
282
+ util.errorRequest(client, request, err)
299
283
  }
300
284
 
301
285
  if (method === 'CONNECT') {
@@ -370,7 +354,7 @@ function writeH2 (client, request) {
370
354
  // A user agent may send a Content-Length header with 0 value, this should be allowed.
371
355
  if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) {
372
356
  if (client[kStrictContentLength]) {
373
- errorRequest(client, request, new RequestContentLengthMismatchError())
357
+ util.errorRequest(client, request, new RequestContentLengthMismatchError())
374
358
  return false
375
359
  }
376
360
 
@@ -412,7 +396,7 @@ function writeH2 (client, request) {
412
396
  // as there's no value to keep it open.
413
397
  if (request.aborted || request.completed) {
414
398
  const err = new RequestAbortedError()
415
- errorRequest(client, request, err)
399
+ util.errorRequest(client, request, err)
416
400
  util.destroy(stream, err)
417
401
  return
418
402
  }
@@ -446,13 +430,12 @@ function writeH2 (client, request) {
446
430
  }
447
431
 
448
432
  const err = new InformationalError('HTTP/2: stream half-closed (remote)')
449
- errorRequest(client, request, err)
433
+ util.errorRequest(client, request, err)
450
434
  util.destroy(stream, err)
451
435
  })
452
436
 
453
437
  stream.once('close', () => {
454
438
  session[kOpenStreams] -= 1
455
- // TODO(HTTP/2): unref only if current streams count is 0
456
439
  if (session[kOpenStreams] === 0) {
457
440
  session.unref()
458
441
  }
@@ -461,13 +444,14 @@ function writeH2 (client, request) {
461
444
  stream.once('error', function (err) {
462
445
  if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
463
446
  session[kOpenStreams] -= 1
447
+ util.errorRequest(client, request, err)
464
448
  util.destroy(stream, err)
465
449
  }
466
450
  })
467
451
 
468
452
  stream.once('frameError', (type, code) => {
469
453
  const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
470
- errorRequest(client, request, err)
454
+ util.errorRequest(client, request, err)
471
455
 
472
456
  if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) {
473
457
  session[kOpenStreams] -= 1
@@ -338,7 +338,7 @@ class Client extends DispatcherBase {
338
338
  const requests = this[kQueue].splice(this[kPendingIdx])
339
339
  for (let i = 0; i < requests.length; i++) {
340
340
  const request = requests[i]
341
- errorRequest(this, request, err)
341
+ util.errorRequest(this, request, err)
342
342
  }
343
343
 
344
344
  const callback = () => {
@@ -378,7 +378,7 @@ function onError (client, err) {
378
378
  const requests = client[kQueue].splice(client[kRunningIdx])
379
379
  for (let i = 0; i < requests.length; i++) {
380
380
  const request = requests[i]
381
- errorRequest(client, request, err)
381
+ util.errorRequest(client, request, err)
382
382
  }
383
383
  assert(client[kSize] === 0)
384
384
  }
@@ -502,7 +502,7 @@ async function connect (client) {
502
502
  assert(client[kRunning] === 0)
503
503
  while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) {
504
504
  const request = client[kQueue][client[kPendingIdx]++]
505
- errorRequest(client, request, err)
505
+ util.errorRequest(client, request, err)
506
506
  }
507
507
  } else {
508
508
  onError(client, err)
@@ -581,7 +581,10 @@ function _resume (client, sync) {
581
581
  }
582
582
 
583
583
  client[kServerName] = request.servername
584
- client[kHTTPContext]?.destroy(new InformationalError('servername changed'))
584
+ client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => {
585
+ client[kHTTPContext] = null
586
+ resume(client)
587
+ })
585
588
  }
586
589
 
587
590
  if (client[kConnecting]) {
@@ -609,13 +612,4 @@ function _resume (client, sync) {
609
612
  }
610
613
  }
611
614
 
612
- function errorRequest (client, request, err) {
613
- try {
614
- request.onError(err)
615
- assert(request.aborted)
616
- } catch (err) {
617
- client.emit('error', err)
618
- }
619
- }
620
-
621
615
  module.exports = Client
@@ -64,19 +64,19 @@ class ProxyAgent extends DispatcherBase {
64
64
  this[kAgent] = new Agent({
65
65
  ...opts,
66
66
  connect: async (opts, callback) => {
67
- let requestedHost = opts.host
67
+ let requestedPath = opts.host
68
68
  if (!opts.port) {
69
- requestedHost += `:${defaultProtocolPort(opts.protocol)}`
69
+ requestedPath += `:${defaultProtocolPort(opts.protocol)}`
70
70
  }
71
71
  try {
72
72
  const { socket, statusCode } = await this[kClient].connect({
73
73
  origin,
74
74
  port,
75
- path: requestedHost,
75
+ path: requestedPath,
76
76
  signal: opts.signal,
77
77
  headers: {
78
78
  ...this[kProxyHeaders],
79
- host: requestedHost
79
+ host: opts.host
80
80
  },
81
81
  servername: this[kProxyTls]?.servername || proxyHostname
82
82
  })
@@ -108,16 +108,18 @@ class ProxyAgent extends DispatcherBase {
108
108
  }
109
109
 
110
110
  dispatch (opts, handler) {
111
- const { host } = new URL(opts.origin)
112
111
  const headers = buildHeaders(opts.headers)
113
112
  throwIfProxyAuthIsSent(headers)
113
+
114
+ if (headers && !('host' in headers) && !('Host' in headers)) {
115
+ const { host } = new URL(opts.origin)
116
+ headers.host = host
117
+ }
118
+
114
119
  return this[kAgent].dispatch(
115
120
  {
116
121
  ...opts,
117
- headers: {
118
- ...headers,
119
- host
120
- }
122
+ headers
121
123
  },
122
124
  handler
123
125
  )
@@ -201,9 +201,9 @@ function shouldRemoveHeader (header, removeContent, unknownOrigin) {
201
201
  if (removeContent && util.headerNameToString(header).startsWith('content-')) {
202
202
  return true
203
203
  }
204
- if (unknownOrigin && (header.length === 13 || header.length === 6)) {
204
+ if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) {
205
205
  const name = util.headerNameToString(header)
206
- return name === 'authorization' || name === 'cookie'
206
+ return name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'
207
207
  }
208
208
  return false
209
209
  }
@@ -90,7 +90,7 @@ class MockInterceptor {
90
90
  this[kContentLength] = false
91
91
  }
92
92
 
93
- createMockScopeDispatchData (statusCode, data, responseOptions = {}) {
93
+ createMockScopeDispatchData ({ statusCode, data, responseOptions }) {
94
94
  const responseData = getResponseData(data)
95
95
  const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {}
96
96
  const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }
@@ -99,14 +99,11 @@ class MockInterceptor {
99
99
  return { statusCode, data, headers, trailers }
100
100
  }
101
101
 
102
- validateReplyParameters (statusCode, data, responseOptions) {
103
- if (typeof statusCode === 'undefined') {
102
+ validateReplyParameters (replyParameters) {
103
+ if (typeof replyParameters.statusCode === 'undefined') {
104
104
  throw new InvalidArgumentError('statusCode must be defined')
105
105
  }
106
- if (typeof data === 'undefined') {
107
- throw new InvalidArgumentError('data must be defined')
108
- }
109
- if (typeof responseOptions !== 'object' || responseOptions === null) {
106
+ if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) {
110
107
  throw new InvalidArgumentError('responseOptions must be an object')
111
108
  }
112
109
  }
@@ -114,28 +111,28 @@ class MockInterceptor {
114
111
  /**
115
112
  * Mock an undici request with a defined reply.
116
113
  */
117
- reply (replyData) {
114
+ reply (replyOptionsCallbackOrStatusCode) {
118
115
  // Values of reply aren't available right now as they
119
116
  // can only be available when the reply callback is invoked.
120
- if (typeof replyData === 'function') {
117
+ if (typeof replyOptionsCallbackOrStatusCode === 'function') {
121
118
  // We'll first wrap the provided callback in another function,
122
119
  // this function will properly resolve the data from the callback
123
120
  // when invoked.
124
121
  const wrappedDefaultsCallback = (opts) => {
125
122
  // Our reply options callback contains the parameter for statusCode, data and options.
126
- const resolvedData = replyData(opts)
123
+ const resolvedData = replyOptionsCallbackOrStatusCode(opts)
127
124
 
128
125
  // Check if it is in the right format
129
- if (typeof resolvedData !== 'object') {
126
+ if (typeof resolvedData !== 'object' || resolvedData === null) {
130
127
  throw new InvalidArgumentError('reply options callback must return an object')
131
128
  }
132
129
 
133
- const { statusCode, data = '', responseOptions = {} } = resolvedData
134
- this.validateReplyParameters(statusCode, data, responseOptions)
130
+ const replyParameters = { data: '', responseOptions: {}, ...resolvedData }
131
+ this.validateReplyParameters(replyParameters)
135
132
  // Since the values can be obtained immediately we return them
136
133
  // from this higher order function that will be resolved later.
137
134
  return {
138
- ...this.createMockScopeDispatchData(statusCode, data, responseOptions)
135
+ ...this.createMockScopeDispatchData(replyParameters)
139
136
  }
140
137
  }
141
138
 
@@ -148,11 +145,15 @@ class MockInterceptor {
148
145
  // we should have 1-3 parameters. So we spread the arguments of
149
146
  // this function to obtain the parameters, since replyData will always
150
147
  // just be the statusCode.
151
- const [statusCode, data = '', responseOptions = {}] = [...arguments]
152
- this.validateReplyParameters(statusCode, data, responseOptions)
148
+ const replyParameters = {
149
+ statusCode: replyOptionsCallbackOrStatusCode,
150
+ data: arguments[1] === undefined ? '' : arguments[1],
151
+ responseOptions: arguments[2] === undefined ? {} : arguments[2]
152
+ }
153
+ this.validateReplyParameters(replyParameters)
153
154
 
154
155
  // Send in-already provided data like usual
155
- const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions)
156
+ const dispatchData = this.createMockScopeDispatchData(replyParameters)
156
157
  const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData)
157
158
  return new MockScope(newMockDispatch)
158
159
  }
@@ -8,7 +8,7 @@ const {
8
8
  kOrigin,
9
9
  kGetNetConnect
10
10
  } = require('./mock-symbols')
11
- const { buildURL, nop } = require('../core/util')
11
+ const { buildURL } = require('../core/util')
12
12
  const { STATUS_CODES } = require('node:http')
13
13
  const {
14
14
  types: {
@@ -285,10 +285,10 @@ function mockDispatch (opts, handler) {
285
285
  const responseHeaders = generateKeyValues(headers)
286
286
  const responseTrailers = generateKeyValues(trailers)
287
287
 
288
- handler.abort = nop
289
- handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode))
290
- handler.onData(Buffer.from(responseData))
291
- handler.onComplete(responseTrailers)
288
+ handler.onConnect?.(err => handler.onError(err), null)
289
+ handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
290
+ handler.onData?.(Buffer.from(responseData))
291
+ handler.onComplete?.(responseTrailers)
292
292
  deleteMockDispatch(mockDispatches, key)
293
293
  }
294
294
 
@@ -14,8 +14,8 @@ const badPorts = [
14
14
  '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
15
15
  '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
16
16
  '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
17
- '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697',
18
- '10080'
17
+ '2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679',
18
+ '6697', '10080'
19
19
  ]
20
20
 
21
21
  const badPortsSet = new Set(badPorts)