undici 6.11.1 → 6.13.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/README.md CHANGED
@@ -248,6 +248,18 @@ const data = {
248
248
  await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
249
249
  ```
250
250
 
251
+ [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) besides text data and buffers can also utilize streams via [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects:
252
+
253
+ ```js
254
+ import { openAsBlob } from 'node:fs'
255
+
256
+ const file = await openAsBlob('./big.csv')
257
+ const body = new FormData()
258
+ body.set('file', file, 'big.csv')
259
+
260
+ await fetch('http://example.com', { method: 'POST', body })
261
+ ```
262
+
251
263
  #### `request.duplex`
252
264
 
253
265
  - half
@@ -969,6 +969,12 @@ Parameters:
969
969
  * **targets** `Array<Dispatcher>`
970
970
  * **error** `Error`
971
971
 
972
+ Emitted when the dispatcher has been disconnected from the origin.
973
+
974
+ > **Note**: For HTTP/2, this event is also emitted when the dispatcher has received the [GOAWAY Frame](https://webconcepts.info/concepts/http2-frame-type/0x7) with an Error with the message `HTTP/2: "GOAWAY" frame received` and the code `UND_ERR_INFO`.
975
+ > Due to nature of the protocol of using binary frames, it is possible that requests gets hanging as a frame can be received between the `HEADER` and `DATA` frames.
976
+ > It is recommended to handle this event and close the dispatcher to create a new HTTP/2 session.
977
+
972
978
  ### Event: `'connectionError'`
973
979
 
974
980
  Parameters:
@@ -8,11 +8,14 @@ function abort (self) {
8
8
  if (self.abort) {
9
9
  self.abort(self[kSignal]?.reason)
10
10
  } else {
11
- self.onError(self[kSignal]?.reason ?? new RequestAbortedError())
11
+ self.reason = self[kSignal]?.reason ?? new RequestAbortedError()
12
12
  }
13
+ removeSignal(self)
13
14
  }
14
15
 
15
16
  function addSignal (self, signal) {
17
+ self.reason = null
18
+
16
19
  self[kSignal] = null
17
20
  self[kListener] = null
18
21
 
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
3
4
  const { AsyncResource } = require('node:async_hooks')
4
- const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
5
+ const { InvalidArgumentError, SocketError } = require('../core/errors')
5
6
  const util = require('../core/util')
6
7
  const { addSignal, removeSignal } = require('./abort-signal')
7
8
 
@@ -32,10 +33,13 @@ class ConnectHandler extends AsyncResource {
32
33
  }
33
34
 
34
35
  onConnect (abort, context) {
35
- if (!this.callback) {
36
- throw new RequestAbortedError()
36
+ if (this.reason) {
37
+ abort(this.reason)
38
+ return
37
39
  }
38
40
 
41
+ assert(this.callback)
42
+
39
43
  this.abort = abort
40
44
  this.context = context
41
45
  }
@@ -147,12 +147,14 @@ class PipelineHandler extends AsyncResource {
147
147
  onConnect (abort, context) {
148
148
  const { ret, res } = this
149
149
 
150
- assert(!res, 'pipeline cannot be retried')
151
-
152
- if (ret.destroyed) {
153
- throw new RequestAbortedError()
150
+ if (this.reason) {
151
+ abort(this.reason)
152
+ return
154
153
  }
155
154
 
155
+ assert(!res, 'pipeline cannot be retried')
156
+ assert(!ret.destroyed)
157
+
156
158
  this.abort = abort
157
159
  this.context = context
158
160
  }
@@ -1,10 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
3
4
  const { Readable } = require('./readable')
4
- const {
5
- InvalidArgumentError,
6
- RequestAbortedError
7
- } = require('../core/errors')
5
+ const { InvalidArgumentError } = require('../core/errors')
8
6
  const util = require('../core/util')
9
7
  const { getResolveErrorBodyCallback } = require('./util')
10
8
  const { AsyncResource } = require('node:async_hooks')
@@ -69,10 +67,13 @@ class RequestHandler extends AsyncResource {
69
67
  }
70
68
 
71
69
  onConnect (abort, context) {
72
- if (!this.callback) {
73
- throw new RequestAbortedError()
70
+ if (this.reason) {
71
+ abort(this.reason)
72
+ return
74
73
  }
75
74
 
75
+ assert(this.callback)
76
+
76
77
  this.abort = abort
77
78
  this.context = context
78
79
  }
@@ -1,11 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const assert = require('node:assert')
3
4
  const { finished, PassThrough } = require('node:stream')
4
- const {
5
- InvalidArgumentError,
6
- InvalidReturnValueError,
7
- RequestAbortedError
8
- } = require('../core/errors')
5
+ const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
9
6
  const util = require('../core/util')
10
7
  const { getResolveErrorBodyCallback } = require('./util')
11
8
  const { AsyncResource } = require('node:async_hooks')
@@ -70,10 +67,13 @@ class StreamHandler extends AsyncResource {
70
67
  }
71
68
 
72
69
  onConnect (abort, context) {
73
- if (!this.callback) {
74
- throw new RequestAbortedError()
70
+ if (this.reason) {
71
+ abort(this.reason)
72
+ return
75
73
  }
76
74
 
75
+ assert(this.callback)
76
+
77
77
  this.abort = abort
78
78
  this.context = context
79
79
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors')
3
+ const { InvalidArgumentError, SocketError } = require('../core/errors')
4
4
  const { AsyncResource } = require('node:async_hooks')
5
5
  const util = require('../core/util')
6
6
  const { addSignal, removeSignal } = require('./abort-signal')
@@ -34,10 +34,13 @@ class UpgradeHandler extends AsyncResource {
34
34
  }
35
35
 
36
36
  onConnect (abort, context) {
37
- if (!this.callback) {
38
- throw new RequestAbortedError()
37
+ if (this.reason) {
38
+ abort(this.reason)
39
+ return
39
40
  }
40
41
 
42
+ assert(this.callback)
43
+
41
44
  this.abort = abort
42
45
  this.context = null
43
46
  }
@@ -63,9 +63,7 @@ class BodyReadable extends Readable {
63
63
  // tick as it is created, then a user who is waiting for a
64
64
  // promise (i.e micro tick) for installing a 'error' listener will
65
65
  // never get a chance and will always encounter an unhandled exception.
66
- // - tick => process.nextTick(fn)
67
- // - micro tick => queueMicrotask(fn)
68
- queueMicrotask(() => {
66
+ setImmediate(() => {
69
67
  callback(err)
70
68
  })
71
69
  }
package/lib/core/util.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const { kDestroyed, kBodyUsed } = require('./symbols')
4
+ const { kDestroyed, kBodyUsed, kListeners } = require('./symbols')
5
5
  const { IncomingMessage } = require('node:http')
6
6
  const stream = require('node:stream')
7
7
  const net = require('node:net')
@@ -22,13 +22,20 @@ function isStream (obj) {
22
22
 
23
23
  // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
24
24
  function isBlobLike (object) {
25
- return (Blob && object instanceof Blob) || (
26
- object &&
27
- typeof object === 'object' &&
28
- (typeof object.stream === 'function' ||
29
- typeof object.arrayBuffer === 'function') &&
30
- /^(Blob|File)$/.test(object[Symbol.toStringTag])
31
- )
25
+ if (object === null) {
26
+ return false
27
+ } else if (object instanceof Blob) {
28
+ return true
29
+ } else if (typeof object !== 'object') {
30
+ return false
31
+ } else {
32
+ const sTag = object[Symbol.toStringTag]
33
+
34
+ return (sTag === 'Blob' || sTag === 'File') && (
35
+ ('stream' in object && typeof object.stream === 'function') ||
36
+ ('arrayBuffer' in object && typeof object.arrayBuffer === 'function')
37
+ )
38
+ }
32
39
  }
33
40
 
34
41
  function buildURL (url, queryParams) {
@@ -534,6 +541,29 @@ function parseRangeHeader (range) {
534
541
  : null
535
542
  }
536
543
 
544
+ function addListener (obj, name, listener) {
545
+ const listeners = (obj[kListeners] ??= [])
546
+ listeners.push([name, listener])
547
+ obj.on(name, listener)
548
+ return obj
549
+ }
550
+
551
+ function removeAllListeners (obj) {
552
+ for (const [name, listener] of obj[kListeners] ?? []) {
553
+ obj.removeListener(name, listener)
554
+ }
555
+ obj[kListeners] = null
556
+ }
557
+
558
+ function errorRequest (client, request, err) {
559
+ try {
560
+ request.onError(err)
561
+ assert(request.aborted)
562
+ } catch (err) {
563
+ client.emit('error', err)
564
+ }
565
+ }
566
+
537
567
  const kEnumerableProperty = Object.create(null)
538
568
  kEnumerableProperty.enumerable = true
539
569
 
@@ -556,6 +586,9 @@ module.exports = {
556
586
  isDestroyed,
557
587
  headerNameToString,
558
588
  bufferToLowerCasedHeaderName,
589
+ addListener,
590
+ removeAllListeners,
591
+ errorRequest,
559
592
  parseRawHeaders,
560
593
  parseHeaders,
561
594
  parseKeepAliveTimeout,
@@ -47,7 +47,6 @@ const {
47
47
  kMaxRequests,
48
48
  kCounter,
49
49
  kMaxResponseSize,
50
- kListeners,
51
50
  kOnError,
52
51
  kResume,
53
52
  kHTTPContext
@@ -56,23 +55,11 @@ const {
56
55
  const constants = require('../llhttp/constants.js')
57
56
  const EMPTY_BUF = Buffer.alloc(0)
58
57
  const FastBuffer = Buffer[Symbol.species]
58
+ const addListener = util.addListener
59
+ const removeAllListeners = util.removeAllListeners
59
60
 
60
61
  let extractBody
61
62
 
62
- function addListener (obj, name, listener) {
63
- const listeners = (obj[kListeners] ??= [])
64
- listeners.push([name, listener])
65
- obj.on(name, listener)
66
- return obj
67
- }
68
-
69
- function removeAllListeners (obj) {
70
- for (const [name, listener] of obj[kListeners] ?? []) {
71
- obj.removeListener(name, listener)
72
- }
73
- obj[kListeners] = null
74
- }
75
-
76
63
  async function lazyllhttp () {
77
64
  const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
78
65
 
@@ -719,14 +706,14 @@ async function connectH1 (client, socket) {
719
706
  const requests = client[kQueue].splice(client[kRunningIdx])
720
707
  for (let i = 0; i < requests.length; i++) {
721
708
  const request = requests[i]
722
- errorRequest(client, request, err)
709
+ util.errorRequest(client, request, err)
723
710
  }
724
711
  } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') {
725
712
  // Fail head of pipeline.
726
713
  const request = client[kQueue][client[kRunningIdx]]
727
714
  client[kQueue][client[kRunningIdx]++] = null
728
715
 
729
- errorRequest(client, request, err)
716
+ util.errorRequest(client, request, err)
730
717
  }
731
718
 
732
719
  client[kPendingIdx] = client[kRunningIdx]
@@ -831,15 +818,6 @@ function resumeH1 (client) {
831
818
  }
832
819
  }
833
820
 
834
- function errorRequest (client, request, err) {
835
- try {
836
- request.onError(err)
837
- assert(request.aborted)
838
- } catch (err) {
839
- client.emit('error', err)
840
- }
841
- }
842
-
843
821
  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
844
822
  function shouldSendContentLength (method) {
845
823
  return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
@@ -906,7 +884,7 @@ function writeH1 (client, request) {
906
884
  // A user agent may send a Content-Length header with 0 value, this should be allowed.
907
885
  if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) {
908
886
  if (client[kStrictContentLength]) {
909
- errorRequest(client, request, new RequestContentLengthMismatchError())
887
+ util.errorRequest(client, request, new RequestContentLengthMismatchError())
910
888
  return false
911
889
  }
912
890
 
@@ -915,22 +893,24 @@ function writeH1 (client, request) {
915
893
 
916
894
  const socket = client[kSocket]
917
895
 
918
- try {
919
- request.onConnect((err) => {
920
- if (request.aborted || request.completed) {
921
- return
922
- }
896
+ const abort = (err) => {
897
+ if (request.aborted || request.completed) {
898
+ return
899
+ }
923
900
 
924
- errorRequest(client, request, err || new RequestAbortedError())
901
+ util.errorRequest(client, request, err || new RequestAbortedError())
925
902
 
926
- util.destroy(socket, new InformationalError('aborted'))
927
- })
903
+ util.destroy(body)
904
+ util.destroy(socket, new InformationalError('aborted'))
905
+ }
906
+
907
+ try {
908
+ request.onConnect(abort)
928
909
  } catch (err) {
929
- errorRequest(client, request, err)
910
+ util.errorRequest(client, request, err)
930
911
  }
931
912
 
932
913
  if (request.aborted) {
933
- util.destroy(body)
934
914
  return false
935
915
  }
936
916
 
@@ -998,35 +978,19 @@ function writeH1 (client, request) {
998
978
 
999
979
  /* istanbul ignore else: assertion */
1000
980
  if (!body || bodyLength === 0) {
1001
- if (contentLength === 0) {
1002
- socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
1003
- } else {
1004
- assert(contentLength === null, 'no body must not have content length')
1005
- socket.write(`${header}\r\n`, 'latin1')
1006
- }
1007
- request.onRequestSent()
981
+ writeBuffer({ abort, body: null, client, request, socket, contentLength, header, expectsPayload })
1008
982
  } else if (util.isBuffer(body)) {
1009
- assert(contentLength === body.byteLength, 'buffer body must have content length')
1010
-
1011
- socket.cork()
1012
- socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
1013
- socket.write(body)
1014
- socket.uncork()
1015
- request.onBodySent(body)
1016
- request.onRequestSent()
1017
- if (!expectsPayload) {
1018
- socket[kReset] = true
1019
- }
983
+ writeBuffer({ abort, body, client, request, socket, contentLength, header, expectsPayload })
1020
984
  } else if (util.isBlobLike(body)) {
1021
985
  if (typeof body.stream === 'function') {
1022
- writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload })
986
+ writeIterable({ abort, body: body.stream(), client, request, socket, contentLength, header, expectsPayload })
1023
987
  } else {
1024
- writeBlob({ body, client, request, socket, contentLength, header, expectsPayload })
988
+ writeBlob({ abort, body, client, request, socket, contentLength, header, expectsPayload })
1025
989
  }
1026
990
  } else if (util.isStream(body)) {
1027
- writeStream({ body, client, request, socket, contentLength, header, expectsPayload })
991
+ writeStream({ abort, body, client, request, socket, contentLength, header, expectsPayload })
1028
992
  } else if (util.isIterable(body)) {
1029
- writeIterable({ body, client, request, socket, contentLength, header, expectsPayload })
993
+ writeIterable({ abort, body, client, request, socket, contentLength, header, expectsPayload })
1030
994
  } else {
1031
995
  assert(false)
1032
996
  }
@@ -1034,12 +998,12 @@ function writeH1 (client, request) {
1034
998
  return true
1035
999
  }
1036
1000
 
1037
- function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
1001
+ function writeStream ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
1038
1002
  assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined')
1039
1003
 
1040
1004
  let finished = false
1041
1005
 
1042
- const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
1006
+ const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
1043
1007
 
1044
1008
  const onData = function (chunk) {
1045
1009
  if (finished) {
@@ -1137,7 +1101,37 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
1137
1101
  }
1138
1102
  }
1139
1103
 
1140
- async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
1104
+ async function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
1105
+ try {
1106
+ if (!body) {
1107
+ if (contentLength === 0) {
1108
+ socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1')
1109
+ } else {
1110
+ assert(contentLength === null, 'no body must not have content length')
1111
+ socket.write(`${header}\r\n`, 'latin1')
1112
+ }
1113
+ } else if (util.isBuffer(body)) {
1114
+ assert(contentLength === body.byteLength, 'buffer body must have content length')
1115
+
1116
+ socket.cork()
1117
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1')
1118
+ socket.write(body)
1119
+ socket.uncork()
1120
+ request.onBodySent(body)
1121
+
1122
+ if (!expectsPayload) {
1123
+ socket[kReset] = true
1124
+ }
1125
+ }
1126
+ request.onRequestSent()
1127
+
1128
+ client[kResume]()
1129
+ } catch (err) {
1130
+ abort(err)
1131
+ }
1132
+ }
1133
+
1134
+ async function writeBlob ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
1141
1135
  assert(contentLength === body.size, 'blob body must have content length')
1142
1136
 
1143
1137
  try {
@@ -1161,11 +1155,11 @@ async function writeBlob ({ h2stream, body, client, request, socket, contentLeng
1161
1155
 
1162
1156
  client[kResume]()
1163
1157
  } catch (err) {
1164
- util.destroy(socket, err)
1158
+ abort(err)
1165
1159
  }
1166
1160
  }
1167
1161
 
1168
- async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) {
1162
+ async function writeIterable ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
1169
1163
  assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined')
1170
1164
 
1171
1165
  let callback = null
@@ -1191,7 +1185,7 @@ async function writeIterable ({ h2stream, body, client, request, socket, content
1191
1185
  .on('close', onDrain)
1192
1186
  .on('drain', onDrain)
1193
1187
 
1194
- const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header })
1188
+ const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header })
1195
1189
  try {
1196
1190
  // It's up to the user to somehow abort the async iterable.
1197
1191
  for await (const chunk of body) {
@@ -1215,7 +1209,7 @@ async function writeIterable ({ h2stream, body, client, request, socket, content
1215
1209
  }
1216
1210
 
1217
1211
  class AsyncWriter {
1218
- constructor ({ socket, request, contentLength, client, expectsPayload, header }) {
1212
+ constructor ({ abort, socket, request, contentLength, client, expectsPayload, header }) {
1219
1213
  this.socket = socket
1220
1214
  this.request = request
1221
1215
  this.contentLength = contentLength
@@ -1223,6 +1217,7 @@ class AsyncWriter {
1223
1217
  this.bytesWritten = 0
1224
1218
  this.expectsPayload = expectsPayload
1225
1219
  this.header = header
1220
+ this.abort = abort
1226
1221
 
1227
1222
  socket[kWriting] = true
1228
1223
  }
@@ -1338,13 +1333,13 @@ class AsyncWriter {
1338
1333
  }
1339
1334
 
1340
1335
  destroy (err) {
1341
- const { socket, client } = this
1336
+ const { socket, client, abort } = this
1342
1337
 
1343
1338
  socket[kWriting] = false
1344
1339
 
1345
1340
  if (err) {
1346
1341
  assert(client[kRunning] <= 1, 'pipeline should only contain this request')
1347
- util.destroy(socket, err)
1342
+ abort(err)
1348
1343
  }
1349
1344
  }
1350
1345
  }