undici 6.20.0 → 7.0.0-alpha.2

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.
Files changed (137) hide show
  1. package/README.md +6 -10
  2. package/docs/docs/api/Agent.md +0 -3
  3. package/docs/docs/api/Client.md +1 -3
  4. package/docs/docs/api/Debug.md +1 -1
  5. package/docs/docs/api/Dispatcher.md +60 -8
  6. package/docs/docs/api/EnvHttpProxyAgent.md +0 -1
  7. package/docs/docs/api/Fetch.md +1 -0
  8. package/docs/docs/api/MockAgent.md +2 -0
  9. package/docs/docs/api/MockPool.md +2 -1
  10. package/docs/docs/api/Pool.md +0 -1
  11. package/docs/docs/api/RetryAgent.md +1 -1
  12. package/docs/docs/api/RetryHandler.md +1 -1
  13. package/docs/docs/api/WebSocket.md +45 -3
  14. package/index.js +6 -6
  15. package/lib/api/abort-signal.js +2 -0
  16. package/lib/api/api-connect.js +3 -1
  17. package/lib/api/api-pipeline.js +7 -6
  18. package/lib/api/api-request.js +32 -47
  19. package/lib/api/api-stream.js +39 -50
  20. package/lib/api/api-upgrade.js +5 -3
  21. package/lib/api/readable.js +261 -64
  22. package/lib/api/util.js +2 -0
  23. package/lib/core/constants.js +11 -9
  24. package/lib/core/diagnostics.js +122 -128
  25. package/lib/core/errors.js +4 -4
  26. package/lib/core/request.js +11 -9
  27. package/lib/core/symbols.js +2 -1
  28. package/lib/core/tree.js +9 -1
  29. package/lib/core/util.js +219 -48
  30. package/lib/dispatcher/agent.js +3 -17
  31. package/lib/dispatcher/balanced-pool.js +5 -8
  32. package/lib/dispatcher/client-h1.js +278 -54
  33. package/lib/dispatcher/client-h2.js +1 -1
  34. package/lib/dispatcher/client.js +23 -34
  35. package/lib/dispatcher/dispatcher-base.js +2 -34
  36. package/lib/dispatcher/dispatcher.js +3 -24
  37. package/lib/dispatcher/fixed-queue.js +91 -49
  38. package/lib/dispatcher/pool-stats.js +2 -0
  39. package/lib/dispatcher/pool.js +3 -6
  40. package/lib/dispatcher/proxy-agent.js +6 -7
  41. package/lib/handler/decorator-handler.js +24 -0
  42. package/lib/handler/redirect-handler.js +11 -2
  43. package/lib/handler/retry-handler.js +12 -3
  44. package/lib/interceptor/dns.js +346 -0
  45. package/lib/interceptor/dump.js +2 -2
  46. package/lib/interceptor/redirect.js +11 -14
  47. package/lib/interceptor/response-error.js +4 -1
  48. package/lib/llhttp/constants.d.ts +97 -0
  49. package/lib/llhttp/constants.js +412 -192
  50. package/lib/llhttp/constants.js.map +1 -0
  51. package/lib/llhttp/llhttp-wasm.js +11 -1
  52. package/lib/llhttp/llhttp_simd-wasm.js +11 -1
  53. package/lib/llhttp/utils.d.ts +2 -0
  54. package/lib/llhttp/utils.js +9 -9
  55. package/lib/llhttp/utils.js.map +1 -0
  56. package/lib/mock/mock-agent.js +5 -8
  57. package/lib/mock/mock-client.js +9 -4
  58. package/lib/mock/mock-errors.js +3 -1
  59. package/lib/mock/mock-interceptor.js +8 -6
  60. package/lib/mock/mock-pool.js +9 -4
  61. package/lib/mock/mock-symbols.js +3 -1
  62. package/lib/mock/mock-utils.js +29 -5
  63. package/lib/web/cache/cache.js +24 -21
  64. package/lib/web/cache/cachestorage.js +1 -1
  65. package/lib/web/cookies/index.js +17 -13
  66. package/lib/web/cookies/parse.js +2 -2
  67. package/lib/web/eventsource/eventsource-stream.js +9 -8
  68. package/lib/web/eventsource/eventsource.js +10 -6
  69. package/lib/web/fetch/body.js +42 -36
  70. package/lib/web/fetch/constants.js +35 -26
  71. package/lib/web/fetch/data-url.js +1 -1
  72. package/lib/web/fetch/formdata-parser.js +2 -2
  73. package/lib/web/fetch/formdata.js +65 -54
  74. package/lib/web/fetch/headers.js +117 -85
  75. package/lib/web/fetch/index.js +55 -62
  76. package/lib/web/fetch/request.js +135 -77
  77. package/lib/web/fetch/response.js +86 -56
  78. package/lib/web/fetch/util.js +90 -64
  79. package/lib/web/fetch/webidl.js +99 -64
  80. package/lib/web/websocket/connection.js +76 -147
  81. package/lib/web/websocket/constants.js +3 -4
  82. package/lib/web/websocket/events.js +4 -2
  83. package/lib/web/websocket/frame.js +45 -3
  84. package/lib/web/websocket/receiver.js +29 -33
  85. package/lib/web/websocket/sender.js +18 -13
  86. package/lib/web/websocket/stream/websocketerror.js +83 -0
  87. package/lib/web/websocket/stream/websocketstream.js +485 -0
  88. package/lib/web/websocket/util.js +128 -77
  89. package/lib/web/websocket/websocket.js +234 -135
  90. package/package.json +20 -33
  91. package/scripts/strip-comments.js +3 -1
  92. package/types/agent.d.ts +7 -7
  93. package/types/api.d.ts +24 -24
  94. package/types/balanced-pool.d.ts +11 -11
  95. package/types/client.d.ts +11 -12
  96. package/types/diagnostics-channel.d.ts +10 -10
  97. package/types/dispatcher.d.ts +96 -97
  98. package/types/env-http-proxy-agent.d.ts +2 -2
  99. package/types/errors.d.ts +53 -47
  100. package/types/fetch.d.ts +8 -8
  101. package/types/formdata.d.ts +7 -7
  102. package/types/global-dispatcher.d.ts +4 -4
  103. package/types/global-origin.d.ts +5 -5
  104. package/types/handlers.d.ts +4 -4
  105. package/types/header.d.ts +157 -1
  106. package/types/index.d.ts +42 -46
  107. package/types/interceptors.d.ts +22 -8
  108. package/types/mock-agent.d.ts +21 -18
  109. package/types/mock-client.d.ts +4 -4
  110. package/types/mock-errors.d.ts +3 -3
  111. package/types/mock-interceptor.d.ts +19 -19
  112. package/types/mock-pool.d.ts +4 -4
  113. package/types/patch.d.ts +0 -4
  114. package/types/pool-stats.d.ts +8 -8
  115. package/types/pool.d.ts +12 -12
  116. package/types/proxy-agent.d.ts +4 -4
  117. package/types/readable.d.ts +22 -14
  118. package/types/retry-agent.d.ts +1 -1
  119. package/types/retry-handler.d.ts +8 -8
  120. package/types/util.d.ts +3 -3
  121. package/types/utility.d.ts +7 -0
  122. package/types/webidl.d.ts +44 -6
  123. package/types/websocket.d.ts +34 -1
  124. package/docs/docs/api/DispatchInterceptor.md +0 -60
  125. package/lib/interceptor/redirect-interceptor.js +0 -21
  126. package/lib/mock/pluralizer.js +0 -29
  127. package/lib/web/cache/symbols.js +0 -5
  128. package/lib/web/fetch/file.js +0 -126
  129. package/lib/web/fetch/symbols.js +0 -9
  130. package/lib/web/fileapi/encoding.js +0 -290
  131. package/lib/web/fileapi/filereader.js +0 -344
  132. package/lib/web/fileapi/progressevent.js +0 -78
  133. package/lib/web/fileapi/symbols.js +0 -10
  134. package/lib/web/fileapi/util.js +0 -391
  135. package/lib/web/websocket/symbols.js +0 -12
  136. package/types/file.d.ts +0 -39
  137. package/types/filereader.d.ts +0 -54
@@ -1,20 +1,21 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const { finished, PassThrough } = require('node:stream')
4
+ const { finished } = require('node:stream')
5
+ const { AsyncResource } = require('node:async_hooks')
5
6
  const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors')
6
7
  const util = require('../core/util')
7
- const { getResolveErrorBodyCallback } = require('./util')
8
- const { AsyncResource } = require('node:async_hooks')
9
8
  const { addSignal, removeSignal } = require('./abort-signal')
10
9
 
10
+ function noop () {}
11
+
11
12
  class StreamHandler extends AsyncResource {
12
13
  constructor (opts, factory, callback) {
13
14
  if (!opts || typeof opts !== 'object') {
14
15
  throw new InvalidArgumentError('invalid opts')
15
16
  }
16
17
 
17
- const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts
18
+ const { signal, method, opaque, body, onInfo, responseHeaders } = opts
18
19
 
19
20
  try {
20
21
  if (typeof callback !== 'function') {
@@ -40,7 +41,7 @@ class StreamHandler extends AsyncResource {
40
41
  super('UNDICI_STREAM')
41
42
  } catch (err) {
42
43
  if (util.isStream(body)) {
43
- util.destroy(body.on('error', util.nop), err)
44
+ util.destroy(body.on('error', noop), err)
44
45
  }
45
46
  throw err
46
47
  }
@@ -55,7 +56,6 @@ class StreamHandler extends AsyncResource {
55
56
  this.trailers = null
56
57
  this.body = body
57
58
  this.onInfo = onInfo || null
58
- this.throwOnError = throwOnError || false
59
59
 
60
60
  if (util.isStream(body)) {
61
61
  body.on('error', (err) => {
@@ -79,7 +79,7 @@ class StreamHandler extends AsyncResource {
79
79
  }
80
80
 
81
81
  onHeaders (statusCode, rawHeaders, resume, statusMessage) {
82
- const { factory, opaque, context, callback, responseHeaders } = this
82
+ const { factory, opaque, context, responseHeaders } = this
83
83
 
84
84
  const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders)
85
85
 
@@ -92,55 +92,42 @@ class StreamHandler extends AsyncResource {
92
92
 
93
93
  this.factory = null
94
94
 
95
- let res
95
+ if (factory === null) {
96
+ return
97
+ }
96
98
 
97
- if (this.throwOnError && statusCode >= 400) {
98
- const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
99
- const contentType = parsedHeaders['content-type']
100
- res = new PassThrough()
99
+ const res = this.runInAsyncScope(factory, null, {
100
+ statusCode,
101
+ headers,
102
+ opaque,
103
+ context
104
+ })
101
105
 
102
- this.callback = null
103
- this.runInAsyncScope(getResolveErrorBodyCallback, null,
104
- { callback, body: res, contentType, statusCode, statusMessage, headers }
105
- )
106
- } else {
107
- if (factory === null) {
108
- return
109
- }
106
+ if (
107
+ !res ||
108
+ typeof res.write !== 'function' ||
109
+ typeof res.end !== 'function' ||
110
+ typeof res.on !== 'function'
111
+ ) {
112
+ throw new InvalidReturnValueError('expected Writable')
113
+ }
110
114
 
111
- res = this.runInAsyncScope(factory, null, {
112
- statusCode,
113
- headers,
114
- opaque,
115
- context
116
- })
115
+ // TODO: Avoid finished. It registers an unnecessary amount of listeners.
116
+ finished(res, { readable: false }, (err) => {
117
+ const { callback, res, opaque, trailers, abort } = this
117
118
 
118
- if (
119
- !res ||
120
- typeof res.write !== 'function' ||
121
- typeof res.end !== 'function' ||
122
- typeof res.on !== 'function'
123
- ) {
124
- throw new InvalidReturnValueError('expected Writable')
119
+ this.res = null
120
+ if (err || !res.readable) {
121
+ util.destroy(res, err)
125
122
  }
126
123
 
127
- // TODO: Avoid finished. It registers an unnecessary amount of listeners.
128
- finished(res, { readable: false }, (err) => {
129
- const { callback, res, opaque, trailers, abort } = this
130
-
131
- this.res = null
132
- if (err || !res.readable) {
133
- util.destroy(res, err)
134
- }
135
-
136
- this.callback = null
137
- this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
124
+ this.callback = null
125
+ this.runInAsyncScope(callback, null, err || null, { opaque, trailers })
138
126
 
139
- if (err) {
140
- abort()
141
- }
142
- })
143
- }
127
+ if (err) {
128
+ abort()
129
+ }
130
+ })
144
131
 
145
132
  res.on('drain', resume)
146
133
 
@@ -207,7 +194,9 @@ function stream (opts, factory, callback) {
207
194
  }
208
195
 
209
196
  try {
210
- this.dispatch(opts, new StreamHandler(opts, factory, callback))
197
+ const handler = new StreamHandler(opts, factory, callback)
198
+
199
+ this.dispatch(opts, handler)
211
200
  } catch (err) {
212
201
  if (typeof callback !== 'function') {
213
202
  throw err
@@ -2,9 +2,9 @@
2
2
 
3
3
  const { InvalidArgumentError, SocketError } = require('../core/errors')
4
4
  const { AsyncResource } = require('node:async_hooks')
5
+ const assert = require('node:assert')
5
6
  const util = require('../core/util')
6
7
  const { addSignal, removeSignal } = require('./abort-signal')
7
- const assert = require('node:assert')
8
8
 
9
9
  class UpgradeHandler extends AsyncResource {
10
10
  constructor (opts, callback) {
@@ -91,11 +91,13 @@ function upgrade (opts, callback) {
91
91
 
92
92
  try {
93
93
  const upgradeHandler = new UpgradeHandler(opts, callback)
94
- this.dispatch({
94
+ const upgradeOpts = {
95
95
  ...opts,
96
96
  method: opts.method || 'GET',
97
97
  upgrade: opts.protocol || 'Websocket'
98
- }, upgradeHandler)
98
+ }
99
+
100
+ this.dispatch(upgradeOpts, upgradeHandler)
99
101
  } catch (err) {
100
102
  if (typeof callback !== 'function') {
101
103
  throw err
@@ -14,10 +14,25 @@ const kBody = Symbol('kBody')
14
14
  const kAbort = Symbol('kAbort')
15
15
  const kContentType = Symbol('kContentType')
16
16
  const kContentLength = Symbol('kContentLength')
17
+ const kUsed = Symbol('kUsed')
18
+ const kBytesRead = Symbol('kBytesRead')
17
19
 
18
20
  const noop = () => {}
19
21
 
22
+ /**
23
+ * @class
24
+ * @extends {Readable}
25
+ * @see https://fetch.spec.whatwg.org/#body
26
+ */
20
27
  class BodyReadable extends Readable {
28
+ /**
29
+ * @param {object} opts
30
+ * @param {(this: Readable, size: number) => void} opts.resume
31
+ * @param {() => (void | null)} opts.abort
32
+ * @param {string} [opts.contentType = '']
33
+ * @param {number} [opts.contentLength]
34
+ * @param {number} [opts.highWaterMark = 64 * 1024]
35
+ */
21
36
  constructor ({
22
37
  resume,
23
38
  abort,
@@ -34,10 +49,19 @@ class BodyReadable extends Readable {
34
49
  this._readableState.dataEmitted = false
35
50
 
36
51
  this[kAbort] = abort
52
+
53
+ /**
54
+ * @type {Consume | null}
55
+ */
37
56
  this[kConsume] = null
57
+ this[kBytesRead] = 0
58
+ /**
59
+ * @type {ReadableStream|null}
60
+ */
38
61
  this[kBody] = null
62
+ this[kUsed] = false
39
63
  this[kContentType] = contentType
40
- this[kContentLength] = contentLength
64
+ this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
41
65
 
42
66
  // Is stream being consumed through Readable API?
43
67
  // This is an optimization so that we avoid checking
@@ -46,7 +70,12 @@ class BodyReadable extends Readable {
46
70
  this[kReading] = false
47
71
  }
48
72
 
49
- destroy (err) {
73
+ /**
74
+ * @param {Error|null} err
75
+ * @param {(error:(Error|null)) => void} callback
76
+ * @returns {void}
77
+ */
78
+ _destroy (err, callback) {
50
79
  if (!err && !this._readableState.endEmitted) {
51
80
  err = new RequestAbortedError()
52
81
  }
@@ -55,15 +84,11 @@ class BodyReadable extends Readable {
55
84
  this[kAbort]()
56
85
  }
57
86
 
58
- return super.destroy(err)
59
- }
60
-
61
- _destroy (err, callback) {
62
87
  // Workaround for Node "bug". If the stream is destroyed in same
63
88
  // tick as it is created, then a user who is waiting for a
64
- // promise (i.e micro tick) for installing a 'error' listener will
89
+ // promise (i.e micro tick) for installing an 'error' listener will
65
90
  // never get a chance and will always encounter an unhandled exception.
66
- if (!this[kReading]) {
91
+ if (!this[kUsed]) {
67
92
  setImmediate(() => {
68
93
  callback(err)
69
94
  })
@@ -72,20 +97,36 @@ class BodyReadable extends Readable {
72
97
  }
73
98
  }
74
99
 
75
- on (ev, ...args) {
76
- if (ev === 'data' || ev === 'readable') {
100
+ /**
101
+ * @param {string} event
102
+ * @param {(...args: any[]) => void} listener
103
+ * @returns {this}
104
+ */
105
+ on (event, listener) {
106
+ if (event === 'data' || event === 'readable') {
77
107
  this[kReading] = true
108
+ this[kUsed] = true
78
109
  }
79
- return super.on(ev, ...args)
110
+ return super.on(event, listener)
80
111
  }
81
112
 
82
- addListener (ev, ...args) {
83
- return this.on(ev, ...args)
113
+ /**
114
+ * @param {string} event
115
+ * @param {(...args: any[]) => void} listener
116
+ * @returns {this}
117
+ */
118
+ addListener (event, listener) {
119
+ return this.on(event, listener)
84
120
  }
85
121
 
86
- off (ev, ...args) {
87
- const ret = super.off(ev, ...args)
88
- if (ev === 'data' || ev === 'readable') {
122
+ /**
123
+ * @param {string|symbol} event
124
+ * @param {(...args: any[]) => void} listener
125
+ * @returns {this}
126
+ */
127
+ off (event, listener) {
128
+ const ret = super.off(event, listener)
129
+ if (event === 'data' || event === 'readable') {
89
130
  this[kReading] = (
90
131
  this.listenerCount('data') > 0 ||
91
132
  this.listenerCount('readable') > 0
@@ -94,11 +135,22 @@ class BodyReadable extends Readable {
94
135
  return ret
95
136
  }
96
137
 
97
- removeListener (ev, ...args) {
98
- return this.off(ev, ...args)
138
+ /**
139
+ * @param {string|symbol} event
140
+ * @param {(...args: any[]) => void} listener
141
+ * @returns {this}
142
+ */
143
+ removeListener (event, listener) {
144
+ return this.off(event, listener)
99
145
  }
100
146
 
147
+ /**
148
+ * @param {Buffer|null} chunk
149
+ * @returns {boolean}
150
+ */
101
151
  push (chunk) {
152
+ this[kBytesRead] += chunk ? chunk.length : 0
153
+
102
154
  if (this[kConsume] && chunk !== null) {
103
155
  consumePush(this[kConsume], chunk)
104
156
  return this[kReading] ? super.push(chunk) : true
@@ -106,38 +158,84 @@ class BodyReadable extends Readable {
106
158
  return super.push(chunk)
107
159
  }
108
160
 
109
- // https://fetch.spec.whatwg.org/#dom-body-text
161
+ /**
162
+ * Consumes and returns the body as a string.
163
+ *
164
+ * @see https://fetch.spec.whatwg.org/#dom-body-text
165
+ * @returns {Promise<string>}
166
+ */
110
167
  async text () {
111
168
  return consume(this, 'text')
112
169
  }
113
170
 
114
- // https://fetch.spec.whatwg.org/#dom-body-json
171
+ /**
172
+ * Consumes and returns the body as a JavaScript Object.
173
+ *
174
+ * @see https://fetch.spec.whatwg.org/#dom-body-json
175
+ * @returns {Promise<unknown>}
176
+ */
115
177
  async json () {
116
178
  return consume(this, 'json')
117
179
  }
118
180
 
119
- // https://fetch.spec.whatwg.org/#dom-body-blob
181
+ /**
182
+ * Consumes and returns the body as a Blob
183
+ *
184
+ * @see https://fetch.spec.whatwg.org/#dom-body-blob
185
+ * @returns {Promise<Blob>}
186
+ */
120
187
  async blob () {
121
188
  return consume(this, 'blob')
122
189
  }
123
190
 
124
- // https://fetch.spec.whatwg.org/#dom-body-arraybuffer
191
+ /**
192
+ * Consumes and returns the body as an Uint8Array.
193
+ *
194
+ * @see https://fetch.spec.whatwg.org/#dom-body-bytes
195
+ * @returns {Promise<Uint8Array>}
196
+ */
197
+ async bytes () {
198
+ return consume(this, 'bytes')
199
+ }
200
+
201
+ /**
202
+ * Consumes and returns the body as an ArrayBuffer.
203
+ *
204
+ * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
205
+ * @returns {Promise<ArrayBuffer>}
206
+ */
125
207
  async arrayBuffer () {
126
208
  return consume(this, 'arrayBuffer')
127
209
  }
128
210
 
129
- // https://fetch.spec.whatwg.org/#dom-body-formdata
211
+ /**
212
+ * Not implemented
213
+ *
214
+ * @see https://fetch.spec.whatwg.org/#dom-body-formdata
215
+ * @throws {NotSupportedError}
216
+ */
130
217
  async formData () {
131
218
  // TODO: Implement.
132
219
  throw new NotSupportedError()
133
220
  }
134
221
 
135
- // https://fetch.spec.whatwg.org/#dom-body-bodyused
222
+ /**
223
+ * Returns true if the body is not null and the body has been consumed.
224
+ * Otherwise, returns false.
225
+ *
226
+ * @see https://fetch.spec.whatwg.org/#dom-body-bodyused
227
+ * @readonly
228
+ * @returns {boolean}
229
+ */
136
230
  get bodyUsed () {
137
231
  return util.isDisturbed(this)
138
232
  }
139
233
 
140
- // https://fetch.spec.whatwg.org/#dom-body-body
234
+ /**
235
+ * @see https://fetch.spec.whatwg.org/#dom-body-body
236
+ * @readonly
237
+ * @returns {ReadableStream}
238
+ */
141
239
  get body () {
142
240
  if (!this[kBody]) {
143
241
  this[kBody] = ReadableStreamFrom(this)
@@ -150,14 +248,24 @@ class BodyReadable extends Readable {
150
248
  return this[kBody]
151
249
  }
152
250
 
251
+ /**
252
+ * Dumps the response body by reading `limit` number of bytes.
253
+ * @param {object} opts
254
+ * @param {number} [opts.limit = 131072] Number of bytes to read.
255
+ * @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump.
256
+ * @returns {Promise<null>}
257
+ */
153
258
  async dump (opts) {
154
- let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024
155
259
  const signal = opts?.signal
156
260
 
157
261
  if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
158
262
  throw new InvalidArgumentError('signal must be an AbortSignal')
159
263
  }
160
264
 
265
+ const limit = opts?.limit && Number.isFinite(opts.limit)
266
+ ? opts.limit
267
+ : 128 * 1024
268
+
161
269
  signal?.throwIfAborted()
162
270
 
163
271
  if (this._readableState.closeEmitted) {
@@ -165,47 +273,88 @@ class BodyReadable extends Readable {
165
273
  }
166
274
 
167
275
  return await new Promise((resolve, reject) => {
168
- if (this[kContentLength] > limit) {
276
+ if (
277
+ (this[kContentLength] && (this[kContentLength] > limit)) ||
278
+ this[kBytesRead] > limit
279
+ ) {
169
280
  this.destroy(new AbortError())
170
281
  }
171
282
 
172
- const onAbort = () => {
173
- this.destroy(signal.reason ?? new AbortError())
283
+ if (signal) {
284
+ const onAbort = () => {
285
+ this.destroy(signal.reason ?? new AbortError())
286
+ }
287
+ signal.addEventListener('abort', onAbort)
288
+ this
289
+ .on('close', function () {
290
+ signal.removeEventListener('abort', onAbort)
291
+ if (signal.aborted) {
292
+ reject(signal.reason ?? new AbortError())
293
+ } else {
294
+ resolve(null)
295
+ }
296
+ })
297
+ } else {
298
+ this.on('close', resolve)
174
299
  }
175
- signal?.addEventListener('abort', onAbort)
176
300
 
177
301
  this
178
- .on('close', function () {
179
- signal?.removeEventListener('abort', onAbort)
180
- if (signal?.aborted) {
181
- reject(signal.reason ?? new AbortError())
182
- } else {
183
- resolve(null)
184
- }
185
- })
186
302
  .on('error', noop)
187
- .on('data', function (chunk) {
188
- limit -= chunk.length
189
- if (limit <= 0) {
303
+ .on('data', () => {
304
+ if (this[kBytesRead] > limit) {
190
305
  this.destroy()
191
306
  }
192
307
  })
193
308
  .resume()
194
309
  })
195
310
  }
311
+
312
+ /**
313
+ * @param {BufferEncoding} encoding
314
+ * @returns {this}
315
+ */
316
+ setEncoding (encoding) {
317
+ if (Buffer.isEncoding(encoding)) {
318
+ this._readableState.encoding = encoding
319
+ }
320
+ return this
321
+ }
196
322
  }
197
323
 
198
- // https://streams.spec.whatwg.org/#readablestream-locked
199
- function isLocked (self) {
324
+ /**
325
+ * @see https://streams.spec.whatwg.org/#readablestream-locked
326
+ * @param {BodyReadable} bodyReadable
327
+ * @returns {boolean}
328
+ */
329
+ function isLocked (bodyReadable) {
200
330
  // Consume is an implicit lock.
201
- return (self[kBody] && self[kBody].locked === true) || self[kConsume]
331
+ return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null
202
332
  }
203
333
 
204
- // https://fetch.spec.whatwg.org/#body-unusable
205
- function isUnusable (self) {
206
- return util.isDisturbed(self) || isLocked(self)
334
+ /**
335
+ * @see https://fetch.spec.whatwg.org/#body-unusable
336
+ * @param {BodyReadable} bodyReadable
337
+ * @returns {boolean}
338
+ */
339
+ function isUnusable (bodyReadable) {
340
+ return util.isDisturbed(bodyReadable) || isLocked(bodyReadable)
207
341
  }
208
342
 
343
+ /**
344
+ * @typedef {object} Consume
345
+ * @property {string} type
346
+ * @property {BodyReadable} stream
347
+ * @property {((value?: any) => void)} resolve
348
+ * @property {((err: Error) => void)} reject
349
+ * @property {number} length
350
+ * @property {Buffer[]} body
351
+ */
352
+
353
+ /**
354
+ * @param {BodyReadable} stream
355
+ * @param {string} type
356
+ * @returns {Promise<any>}
357
+ */
209
358
  async function consume (stream, type) {
210
359
  assert(!stream[kConsume])
211
360
 
@@ -250,6 +399,10 @@ async function consume (stream, type) {
250
399
  })
251
400
  }
252
401
 
402
+ /**
403
+ * @param {Consume} consume
404
+ * @returns {void}
405
+ */
253
406
  function consumeStart (consume) {
254
407
  if (consume.body === null) {
255
408
  return
@@ -270,10 +423,10 @@ function consumeStart (consume) {
270
423
  }
271
424
 
272
425
  if (state.endEmitted) {
273
- consumeEnd(this[kConsume])
426
+ consumeEnd(this[kConsume], this._readableState.encoding)
274
427
  } else {
275
428
  consume.stream.on('end', function () {
276
- consumeEnd(this[kConsume])
429
+ consumeEnd(this[kConsume], this._readableState.encoding)
277
430
  })
278
431
  }
279
432
 
@@ -287,8 +440,10 @@ function consumeStart (consume) {
287
440
  /**
288
441
  * @param {Buffer[]} chunks
289
442
  * @param {number} length
443
+ * @param {BufferEncoding} encoding
444
+ * @returns {string}
290
445
  */
291
- function chunksDecode (chunks, length) {
446
+ function chunksDecode (chunks, length, encoding) {
292
447
  if (chunks.length === 0 || length === 0) {
293
448
  return ''
294
449
  }
@@ -303,29 +458,57 @@ function chunksDecode (chunks, length) {
303
458
  buffer[2] === 0xbf
304
459
  ? 3
305
460
  : 0
306
- return buffer.utf8Slice(start, bufferLength)
461
+ if (!encoding || encoding === 'utf8' || encoding === 'utf-8') {
462
+ return buffer.utf8Slice(start, bufferLength)
463
+ } else {
464
+ return buffer.subarray(start, bufferLength).toString(encoding)
465
+ }
466
+ }
467
+
468
+ /**
469
+ * @param {Buffer[]} chunks
470
+ * @param {number} length
471
+ * @returns {Uint8Array}
472
+ */
473
+ function chunksConcat (chunks, length) {
474
+ if (chunks.length === 0 || length === 0) {
475
+ return new Uint8Array(0)
476
+ }
477
+ if (chunks.length === 1) {
478
+ // fast-path
479
+ return new Uint8Array(chunks[0])
480
+ }
481
+ const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer)
482
+
483
+ let offset = 0
484
+ for (let i = 0; i < chunks.length; ++i) {
485
+ const chunk = chunks[i]
486
+ buffer.set(chunk, offset)
487
+ offset += chunk.length
488
+ }
489
+
490
+ return buffer
307
491
  }
308
492
 
309
- function consumeEnd (consume) {
493
+ /**
494
+ * @param {Consume} consume
495
+ * @param {BufferEncoding} encoding
496
+ * @returns {void}
497
+ */
498
+ function consumeEnd (consume, encoding) {
310
499
  const { type, body, resolve, stream, length } = consume
311
500
 
312
501
  try {
313
502
  if (type === 'text') {
314
- resolve(chunksDecode(body, length))
503
+ resolve(chunksDecode(body, length, encoding))
315
504
  } else if (type === 'json') {
316
- resolve(JSON.parse(chunksDecode(body, length)))
505
+ resolve(JSON.parse(chunksDecode(body, length, encoding)))
317
506
  } else if (type === 'arrayBuffer') {
318
- const dst = new Uint8Array(length)
319
-
320
- let pos = 0
321
- for (const buf of body) {
322
- dst.set(buf, pos)
323
- pos += buf.byteLength
324
- }
325
-
326
- resolve(dst.buffer)
507
+ resolve(chunksConcat(body, length).buffer)
327
508
  } else if (type === 'blob') {
328
509
  resolve(new Blob(body, { type: stream[kContentType] }))
510
+ } else if (type === 'bytes') {
511
+ resolve(chunksConcat(body, length))
329
512
  }
330
513
 
331
514
  consumeFinish(consume)
@@ -334,11 +517,21 @@ function consumeEnd (consume) {
334
517
  }
335
518
  }
336
519
 
520
+ /**
521
+ * @param {Consume} consume
522
+ * @param {Buffer} chunk
523
+ * @returns {void}
524
+ */
337
525
  function consumePush (consume, chunk) {
338
526
  consume.length += chunk.length
339
527
  consume.body.push(chunk)
340
528
  }
341
529
 
530
+ /**
531
+ * @param {Consume} consume
532
+ * @param {Error} [err]
533
+ * @returns {void}
534
+ */
342
535
  function consumeFinish (consume, err) {
343
536
  if (consume.body === null) {
344
537
  return
@@ -350,6 +543,7 @@ function consumeFinish (consume, err) {
350
543
  consume.resolve()
351
544
  }
352
545
 
546
+ // Reset the consume object to allow for garbage collection.
353
547
  consume.type = null
354
548
  consume.stream = null
355
549
  consume.resolve = null
@@ -358,4 +552,7 @@ function consumeFinish (consume, err) {
358
552
  consume.body = null
359
553
  }
360
554
 
361
- module.exports = { Readable: BodyReadable, chunksDecode }
555
+ module.exports = {
556
+ Readable: BodyReadable,
557
+ chunksDecode
558
+ }
package/lib/api/util.js CHANGED
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const assert = require('node:assert')
2
4
  const {
3
5
  ResponseStatusCodeError