undici 6.21.0 → 7.0.0-alpha.10

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 (156) hide show
  1. package/README.md +27 -46
  2. package/docs/docs/api/Agent.md +14 -17
  3. package/docs/docs/api/BalancedPool.md +16 -16
  4. package/docs/docs/api/CacheStore.md +131 -0
  5. package/docs/docs/api/Client.md +12 -14
  6. package/docs/docs/api/Debug.md +1 -1
  7. package/docs/docs/api/Dispatcher.md +98 -194
  8. package/docs/docs/api/EnvHttpProxyAgent.md +12 -13
  9. package/docs/docs/api/MockAgent.md +5 -3
  10. package/docs/docs/api/MockClient.md +5 -5
  11. package/docs/docs/api/MockPool.md +4 -3
  12. package/docs/docs/api/Pool.md +15 -16
  13. package/docs/docs/api/PoolStats.md +1 -1
  14. package/docs/docs/api/ProxyAgent.md +3 -3
  15. package/docs/docs/api/RedirectHandler.md +1 -1
  16. package/docs/docs/api/RetryAgent.md +1 -1
  17. package/docs/docs/api/RetryHandler.md +4 -4
  18. package/docs/docs/api/WebSocket.md +46 -4
  19. package/docs/docs/api/api-lifecycle.md +11 -11
  20. package/docs/docs/best-practices/mocking-request.md +2 -2
  21. package/docs/docs/best-practices/proxy.md +1 -1
  22. package/index.d.ts +1 -1
  23. package/index.js +23 -7
  24. package/lib/api/abort-signal.js +2 -0
  25. package/lib/api/api-connect.js +3 -1
  26. package/lib/api/api-pipeline.js +7 -6
  27. package/lib/api/api-request.js +33 -48
  28. package/lib/api/api-stream.js +39 -50
  29. package/lib/api/api-upgrade.js +5 -3
  30. package/lib/api/readable.js +235 -62
  31. package/lib/api/util.js +2 -0
  32. package/lib/cache/memory-cache-store.js +177 -0
  33. package/lib/cache/sqlite-cache-store.js +446 -0
  34. package/lib/core/constants.js +35 -10
  35. package/lib/core/diagnostics.js +122 -128
  36. package/lib/core/errors.js +6 -6
  37. package/lib/core/request.js +13 -11
  38. package/lib/core/symbols.js +2 -1
  39. package/lib/core/tree.js +9 -1
  40. package/lib/core/util.js +237 -49
  41. package/lib/dispatcher/agent.js +3 -17
  42. package/lib/dispatcher/balanced-pool.js +5 -8
  43. package/lib/dispatcher/client-h1.js +379 -134
  44. package/lib/dispatcher/client-h2.js +173 -107
  45. package/lib/dispatcher/client.js +19 -32
  46. package/lib/dispatcher/dispatcher-base.js +6 -35
  47. package/lib/dispatcher/dispatcher.js +7 -24
  48. package/lib/dispatcher/fixed-queue.js +91 -49
  49. package/lib/dispatcher/pool-stats.js +2 -0
  50. package/lib/dispatcher/pool.js +3 -6
  51. package/lib/dispatcher/proxy-agent.js +3 -6
  52. package/lib/handler/cache-handler.js +393 -0
  53. package/lib/handler/cache-revalidation-handler.js +124 -0
  54. package/lib/handler/decorator-handler.js +27 -0
  55. package/lib/handler/redirect-handler.js +54 -59
  56. package/lib/handler/retry-handler.js +77 -109
  57. package/lib/handler/unwrap-handler.js +96 -0
  58. package/lib/handler/wrap-handler.js +98 -0
  59. package/lib/interceptor/cache.js +350 -0
  60. package/lib/interceptor/dns.js +375 -0
  61. package/lib/interceptor/dump.js +2 -2
  62. package/lib/interceptor/redirect.js +11 -14
  63. package/lib/interceptor/response-error.js +18 -7
  64. package/lib/llhttp/constants.d.ts +97 -0
  65. package/lib/llhttp/constants.js +412 -192
  66. package/lib/llhttp/constants.js.map +1 -0
  67. package/lib/llhttp/llhttp-wasm.js +11 -1
  68. package/lib/llhttp/llhttp_simd-wasm.js +11 -1
  69. package/lib/llhttp/utils.d.ts +2 -0
  70. package/lib/llhttp/utils.js +9 -9
  71. package/lib/llhttp/utils.js.map +1 -0
  72. package/lib/mock/mock-agent.js +5 -8
  73. package/lib/mock/mock-client.js +9 -4
  74. package/lib/mock/mock-errors.js +3 -1
  75. package/lib/mock/mock-interceptor.js +8 -6
  76. package/lib/mock/mock-pool.js +9 -4
  77. package/lib/mock/mock-symbols.js +3 -1
  78. package/lib/mock/mock-utils.js +29 -5
  79. package/lib/util/cache.js +360 -0
  80. package/lib/web/cache/cache.js +24 -21
  81. package/lib/web/cache/cachestorage.js +1 -1
  82. package/lib/web/cookies/index.js +29 -14
  83. package/lib/web/cookies/parse.js +8 -3
  84. package/lib/web/eventsource/eventsource-stream.js +9 -8
  85. package/lib/web/eventsource/eventsource.js +10 -6
  86. package/lib/web/fetch/body.js +43 -41
  87. package/lib/web/fetch/constants.js +12 -5
  88. package/lib/web/fetch/data-url.js +3 -3
  89. package/lib/web/fetch/formdata-parser.js +72 -45
  90. package/lib/web/fetch/formdata.js +65 -54
  91. package/lib/web/fetch/headers.js +118 -86
  92. package/lib/web/fetch/index.js +58 -67
  93. package/lib/web/fetch/request.js +136 -77
  94. package/lib/web/fetch/response.js +87 -56
  95. package/lib/web/fetch/util.js +259 -109
  96. package/lib/web/fetch/webidl.js +113 -68
  97. package/lib/web/websocket/connection.js +76 -147
  98. package/lib/web/websocket/constants.js +70 -10
  99. package/lib/web/websocket/events.js +4 -2
  100. package/lib/web/websocket/frame.js +45 -3
  101. package/lib/web/websocket/receiver.js +29 -33
  102. package/lib/web/websocket/sender.js +18 -13
  103. package/lib/web/websocket/stream/websocketerror.js +83 -0
  104. package/lib/web/websocket/stream/websocketstream.js +485 -0
  105. package/lib/web/websocket/util.js +128 -77
  106. package/lib/web/websocket/websocket.js +234 -135
  107. package/package.json +24 -36
  108. package/scripts/strip-comments.js +3 -1
  109. package/types/agent.d.ts +7 -7
  110. package/types/api.d.ts +24 -24
  111. package/types/balanced-pool.d.ts +11 -11
  112. package/types/cache-interceptor.d.ts +172 -0
  113. package/types/client.d.ts +11 -12
  114. package/types/cookies.d.ts +2 -0
  115. package/types/diagnostics-channel.d.ts +10 -10
  116. package/types/dispatcher.d.ts +113 -90
  117. package/types/env-http-proxy-agent.d.ts +2 -2
  118. package/types/errors.d.ts +53 -47
  119. package/types/fetch.d.ts +17 -16
  120. package/types/formdata.d.ts +7 -7
  121. package/types/global-dispatcher.d.ts +4 -4
  122. package/types/global-origin.d.ts +5 -5
  123. package/types/handlers.d.ts +7 -7
  124. package/types/header.d.ts +157 -1
  125. package/types/index.d.ts +44 -46
  126. package/types/interceptors.d.ts +25 -8
  127. package/types/mock-agent.d.ts +21 -18
  128. package/types/mock-client.d.ts +4 -4
  129. package/types/mock-errors.d.ts +3 -3
  130. package/types/mock-interceptor.d.ts +19 -19
  131. package/types/mock-pool.d.ts +4 -4
  132. package/types/patch.d.ts +0 -4
  133. package/types/pool-stats.d.ts +8 -8
  134. package/types/pool.d.ts +12 -12
  135. package/types/proxy-agent.d.ts +4 -4
  136. package/types/readable.d.ts +18 -15
  137. package/types/retry-agent.d.ts +1 -1
  138. package/types/retry-handler.d.ts +10 -10
  139. package/types/util.d.ts +3 -3
  140. package/types/utility.d.ts +7 -0
  141. package/types/webidl.d.ts +44 -6
  142. package/types/websocket.d.ts +34 -1
  143. package/docs/docs/api/DispatchInterceptor.md +0 -60
  144. package/lib/interceptor/redirect-interceptor.js +0 -21
  145. package/lib/mock/pluralizer.js +0 -29
  146. package/lib/web/cache/symbols.js +0 -5
  147. package/lib/web/fetch/file.js +0 -126
  148. package/lib/web/fetch/symbols.js +0 -9
  149. package/lib/web/fileapi/encoding.js +0 -290
  150. package/lib/web/fileapi/filereader.js +0 -344
  151. package/lib/web/fileapi/progressevent.js +0 -78
  152. package/lib/web/fileapi/symbols.js +0 -10
  153. package/lib/web/fileapi/util.js +0 -391
  154. package/lib/web/websocket/symbols.js +0 -12
  155. package/types/file.d.ts +0 -39
  156. 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,43 +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
110
- async 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
+ */
167
+ text () {
111
168
  return consume(this, 'text')
112
169
  }
113
170
 
114
- // https://fetch.spec.whatwg.org/#dom-body-json
115
- async 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
+ */
177
+ json () {
116
178
  return consume(this, 'json')
117
179
  }
118
180
 
119
- // https://fetch.spec.whatwg.org/#dom-body-blob
120
- async 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
+ */
187
+ blob () {
121
188
  return consume(this, 'blob')
122
189
  }
123
190
 
124
- // https://fetch.spec.whatwg.org/#dom-body-bytes
125
- async bytes () {
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
+ bytes () {
126
198
  return consume(this, 'bytes')
127
199
  }
128
200
 
129
- // https://fetch.spec.whatwg.org/#dom-body-arraybuffer
130
- async arrayBuffer () {
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
+ */
207
+ arrayBuffer () {
131
208
  return consume(this, 'arrayBuffer')
132
209
  }
133
210
 
134
- // 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
+ */
135
217
  async formData () {
136
218
  // TODO: Implement.
137
219
  throw new NotSupportedError()
138
220
  }
139
221
 
140
- // 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
+ */
141
230
  get bodyUsed () {
142
231
  return util.isDisturbed(this)
143
232
  }
144
233
 
145
- // 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
+ */
146
239
  get body () {
147
240
  if (!this[kBody]) {
148
241
  this[kBody] = ReadableStreamFrom(this)
@@ -155,14 +248,24 @@ class BodyReadable extends Readable {
155
248
  return this[kBody]
156
249
  }
157
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
+ */
158
258
  async dump (opts) {
159
- let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024
160
259
  const signal = opts?.signal
161
260
 
162
261
  if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
163
262
  throw new InvalidArgumentError('signal must be an AbortSignal')
164
263
  }
165
264
 
265
+ const limit = opts?.limit && Number.isFinite(opts.limit)
266
+ ? opts.limit
267
+ : 128 * 1024
268
+
166
269
  signal?.throwIfAborted()
167
270
 
168
271
  if (this._readableState.closeEmitted) {
@@ -170,48 +273,89 @@ class BodyReadable extends Readable {
170
273
  }
171
274
 
172
275
  return await new Promise((resolve, reject) => {
173
- if (this[kContentLength] > limit) {
276
+ if (
277
+ (this[kContentLength] && (this[kContentLength] > limit)) ||
278
+ this[kBytesRead] > limit
279
+ ) {
174
280
  this.destroy(new AbortError())
175
281
  }
176
282
 
177
- const onAbort = () => {
178
- 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)
179
299
  }
180
- signal?.addEventListener('abort', onAbort)
181
300
 
182
301
  this
183
- .on('close', function () {
184
- signal?.removeEventListener('abort', onAbort)
185
- if (signal?.aborted) {
186
- reject(signal.reason ?? new AbortError())
187
- } else {
188
- resolve(null)
189
- }
190
- })
191
302
  .on('error', noop)
192
- .on('data', function (chunk) {
193
- limit -= chunk.length
194
- if (limit <= 0) {
303
+ .on('data', () => {
304
+ if (this[kBytesRead] > limit) {
195
305
  this.destroy()
196
306
  }
197
307
  })
198
308
  .resume()
199
309
  })
200
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
+ }
201
322
  }
202
323
 
203
- // https://streams.spec.whatwg.org/#readablestream-locked
204
- 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) {
205
330
  // Consume is an implicit lock.
206
- return (self[kBody] && self[kBody].locked === true) || self[kConsume]
331
+ return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null
207
332
  }
208
333
 
209
- // https://fetch.spec.whatwg.org/#body-unusable
210
- function isUnusable (self) {
211
- 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)
212
341
  }
213
342
 
214
- async function consume (stream, type) {
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
+ */
358
+ function consume (stream, type) {
215
359
  assert(!stream[kConsume])
216
360
 
217
361
  return new Promise((resolve, reject) => {
@@ -255,6 +399,10 @@ async function consume (stream, type) {
255
399
  })
256
400
  }
257
401
 
402
+ /**
403
+ * @param {Consume} consume
404
+ * @returns {void}
405
+ */
258
406
  function consumeStart (consume) {
259
407
  if (consume.body === null) {
260
408
  return
@@ -275,10 +423,10 @@ function consumeStart (consume) {
275
423
  }
276
424
 
277
425
  if (state.endEmitted) {
278
- consumeEnd(this[kConsume])
426
+ consumeEnd(this[kConsume], this._readableState.encoding)
279
427
  } else {
280
428
  consume.stream.on('end', function () {
281
- consumeEnd(this[kConsume])
429
+ consumeEnd(this[kConsume], this._readableState.encoding)
282
430
  })
283
431
  }
284
432
 
@@ -292,8 +440,10 @@ function consumeStart (consume) {
292
440
  /**
293
441
  * @param {Buffer[]} chunks
294
442
  * @param {number} length
443
+ * @param {BufferEncoding} encoding
444
+ * @returns {string}
295
445
  */
296
- function chunksDecode (chunks, length) {
446
+ function chunksDecode (chunks, length, encoding) {
297
447
  if (chunks.length === 0 || length === 0) {
298
448
  return ''
299
449
  }
@@ -308,7 +458,11 @@ function chunksDecode (chunks, length) {
308
458
  buffer[2] === 0xbf
309
459
  ? 3
310
460
  : 0
311
- 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
+ }
312
466
  }
313
467
 
314
468
  /**
@@ -336,14 +490,19 @@ function chunksConcat (chunks, length) {
336
490
  return buffer
337
491
  }
338
492
 
339
- function consumeEnd (consume) {
493
+ /**
494
+ * @param {Consume} consume
495
+ * @param {BufferEncoding} encoding
496
+ * @returns {void}
497
+ */
498
+ function consumeEnd (consume, encoding) {
340
499
  const { type, body, resolve, stream, length } = consume
341
500
 
342
501
  try {
343
502
  if (type === 'text') {
344
- resolve(chunksDecode(body, length))
503
+ resolve(chunksDecode(body, length, encoding))
345
504
  } else if (type === 'json') {
346
- resolve(JSON.parse(chunksDecode(body, length)))
505
+ resolve(JSON.parse(chunksDecode(body, length, encoding)))
347
506
  } else if (type === 'arrayBuffer') {
348
507
  resolve(chunksConcat(body, length).buffer)
349
508
  } else if (type === 'blob') {
@@ -358,11 +517,21 @@ function consumeEnd (consume) {
358
517
  }
359
518
  }
360
519
 
520
+ /**
521
+ * @param {Consume} consume
522
+ * @param {Buffer} chunk
523
+ * @returns {void}
524
+ */
361
525
  function consumePush (consume, chunk) {
362
526
  consume.length += chunk.length
363
527
  consume.body.push(chunk)
364
528
  }
365
529
 
530
+ /**
531
+ * @param {Consume} consume
532
+ * @param {Error} [err]
533
+ * @returns {void}
534
+ */
366
535
  function consumeFinish (consume, err) {
367
536
  if (consume.body === null) {
368
537
  return
@@ -374,6 +543,7 @@ function consumeFinish (consume, err) {
374
543
  consume.resolve()
375
544
  }
376
545
 
546
+ // Reset the consume object to allow for garbage collection.
377
547
  consume.type = null
378
548
  consume.stream = null
379
549
  consume.resolve = null
@@ -382,4 +552,7 @@ function consumeFinish (consume, err) {
382
552
  consume.body = null
383
553
  }
384
554
 
385
- 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