@nxtedition/nxt-undici 4.2.26 → 5.0.1

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.
@@ -14,7 +14,7 @@ class Handler extends DecoratorHandler {
14
14
  #reason = null
15
15
  #headersSent = false
16
16
  #count = 0
17
- #location = null
17
+ #location = ''
18
18
  #history = []
19
19
 
20
20
  constructor(opts, { dispatch, handler }) {
@@ -23,7 +23,7 @@ class Handler extends DecoratorHandler {
23
23
  this.#dispatch = dispatch
24
24
  this.#handler = handler
25
25
  this.#opts = opts
26
- this.#maxCount = Number.isFinite(opts.follow) ? opts.follow : opts.follow?.count ?? 0
26
+ this.#maxCount = Number.isFinite(opts.follow) ? opts.follow : (opts.follow?.count ?? 0)
27
27
 
28
28
  this.#handler.onConnect((reason) => {
29
29
  this.#aborted = true
@@ -51,14 +51,14 @@ class Handler extends DecoratorHandler {
51
51
  if (redirectableStatusCodes.indexOf(statusCode) === -1) {
52
52
  assert(!this.#headersSent)
53
53
  this.#headersSent = true
54
- return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
54
+ return this.#handler.onHeaders(statusCode, null, resume, statusText, headers)
55
55
  }
56
56
 
57
57
  if (isDisturbed(this.#opts.body)) {
58
58
  throw new Error(`Disturbed request cannot be redirected.`)
59
59
  }
60
60
 
61
- this.#location = headers.location
61
+ this.#location = typeof headers.location === 'string' ? headers.location : ''
62
62
 
63
63
  if (!this.#location) {
64
64
  throw new Error(`Missing redirection location .`)
@@ -71,7 +71,7 @@ class Handler extends DecoratorHandler {
71
71
  if (!this.#opts.follow(this.#location, this.#count, this.#opts)) {
72
72
  assert(!this.#headersSent)
73
73
  this.#headersSent = true
74
- return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusText, headers)
74
+ return this.#handler.onHeaders(statusCode, null, resume, statusText, headers)
75
75
  }
76
76
  } else {
77
77
  if (this.#count >= this.#maxCount) {
@@ -5,9 +5,9 @@ class Handler extends DecoratorHandler {
5
5
  #handler
6
6
 
7
7
  #statusCode = 0
8
- #contentType = null
9
- #decoder = null
10
- #headers = null
8
+ #contentType
9
+ #decoder
10
+ #headers
11
11
  #body = ''
12
12
  #opts
13
13
  #errored = false
@@ -36,7 +36,7 @@ class Handler extends DecoratorHandler {
36
36
  this.#contentType = headers['content-type']
37
37
 
38
38
  if (this.#statusCode < 400) {
39
- return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
39
+ return this.#handler.onHeaders(statusCode, null, resume, statusMessage, headers)
40
40
  }
41
41
 
42
42
  // TODO (fix): Check content length
@@ -68,18 +68,18 @@ class Handler extends DecoratorHandler {
68
68
  assert(this.#headersSent === false)
69
69
 
70
70
  if (headers.trailer) {
71
- return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
71
+ return this.#onHeaders(statusCode, null, resume, statusMessage, headers)
72
72
  }
73
73
 
74
74
  const contentLength = headers['content-length'] ? Number(headers['content-length']) : null
75
75
  if (contentLength != null && !Number.isFinite(contentLength)) {
76
- return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
76
+ return this.#onHeaders(statusCode, null, resume, statusMessage, headers)
77
77
  }
78
78
 
79
79
  if (statusCode === 206) {
80
80
  const range = parseRangeHeader(headers['content-range'])
81
81
  if (!range) {
82
- return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
82
+ return this.#onHeaders(statusCode, null, resume, statusMessage, headers)
83
83
  }
84
84
 
85
85
  const { start, size, end = size } = range
@@ -99,7 +99,7 @@ class Handler extends DecoratorHandler {
99
99
  this.#end = contentLength
100
100
  this.#etag = headers.etag
101
101
  } else {
102
- return this.#onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
102
+ return this.#onHeaders(statusCode, null, resume, statusMessage, headers)
103
103
  }
104
104
 
105
105
  // Weak etags are not useful for comparison nor cache
@@ -8,18 +8,18 @@ class Handler extends DecoratorHandler {
8
8
  #handler
9
9
 
10
10
  #verifyOpts
11
- #contentMD5 = null
12
- #contentLength = null
13
- #hasher = null
11
+ #contentMD5
12
+ #contentLength
13
+ #hasher
14
14
  #pos = 0
15
- #errored = false
15
+ #errorSent = false
16
16
 
17
17
  constructor(opts, { handler }) {
18
18
  super(handler)
19
19
 
20
20
  this.#handler = handler
21
21
  this.#verifyOpts =
22
- opts.verify === true ? { hash: true, size: true } : opts.verify ?? DEFAULT_OPTS
22
+ opts.verify === true ? { hash: true, size: true } : (opts.verify ?? DEFAULT_OPTS)
23
23
  }
24
24
 
25
25
  onConnect(abort) {
@@ -29,7 +29,7 @@ class Handler extends DecoratorHandler {
29
29
  this.#contentLength = null
30
30
  this.#hasher = null
31
31
  this.#pos = 0
32
- this.#errored = false
32
+ this.#errorSent = false
33
33
 
34
34
  this.#handler.onConnect(abort)
35
35
  }
@@ -39,7 +39,7 @@ class Handler extends DecoratorHandler {
39
39
  this.#contentLength = this.#verifyOpts.hash ? headers['content-length'] : null
40
40
  this.#hasher = this.#contentMD5 != null ? crypto.createHash('md5') : null
41
41
 
42
- return this.#handler.onHeaders(statusCode, rawHeaders, resume, statusMessage, headers)
42
+ return this.#handler.onHeaders(statusCode, null, resume, statusMessage, headers)
43
43
  }
44
44
 
45
45
  onData(chunk) {
@@ -49,11 +49,11 @@ class Handler extends DecoratorHandler {
49
49
  return this.#handler.onData(chunk)
50
50
  }
51
51
 
52
- onComplete(rawTrailers) {
52
+ onComplete() {
53
53
  const contentMD5 = this.#hasher?.digest('base64')
54
54
 
55
55
  if (this.#contentLength != null && this.#pos !== Number(this.#contentLength)) {
56
- this.#errored = true
56
+ this.#errorSent = true
57
57
  this.#handler.onError(
58
58
  Object.assign(new Error('Request Content-Length mismatch'), {
59
59
  expected: Number(this.#contentLength),
@@ -61,7 +61,7 @@ class Handler extends DecoratorHandler {
61
61
  }),
62
62
  )
63
63
  } else if (this.#contentMD5 != null && contentMD5 !== this.#contentMD5) {
64
- this.#errored = true
64
+ this.#errorSent = true
65
65
  this.#handler.onError(
66
66
  Object.assign(new Error('Request Content-MD5 mismatch'), {
67
67
  expected: this.#contentMD5,
@@ -69,12 +69,13 @@ class Handler extends DecoratorHandler {
69
69
  }),
70
70
  )
71
71
  } else {
72
- return this.#handler.onComplete(rawTrailers)
72
+ return this.#handler.onComplete()
73
73
  }
74
74
  }
75
75
 
76
76
  onError(err) {
77
- if (!this.#errored) {
77
+ if (!this.#errorSent) {
78
+ this.#errorSent = true
78
79
  this.#handler.onError(err)
79
80
  }
80
81
  }
@@ -0,0 +1,523 @@
1
+ // Ported from https://github.com/nodejs/undici/pull/907
2
+
3
+ import assert from 'node:assert'
4
+ import { Readable } from 'node:stream'
5
+ import {
6
+ RequestAbortedError,
7
+ NotSupportedError,
8
+ InvalidArgumentError,
9
+ AbortError,
10
+ } from './errors.js'
11
+ import { isDisturbed } from './utils.js'
12
+
13
+ const kConsume = Symbol('kConsume')
14
+ const kReading = Symbol('kReading')
15
+ const kBody = Symbol('kBody')
16
+ const kAbort = Symbol('kAbort')
17
+ const kContentType = Symbol('kContentType')
18
+ const kContentLength = Symbol('kContentLength')
19
+ const kUsed = Symbol('kUsed')
20
+ const kBytesRead = Symbol('kBytesRead')
21
+
22
+ const noop = () => {}
23
+
24
+ /**
25
+ * @class
26
+ * @extends {Readable}
27
+ * @see https://fetch.spec.whatwg.org/#body
28
+ */
29
+ export class BodyReadable extends Readable {
30
+ /**
31
+ * @param {object} opts
32
+ * @param {(this: Readable, size: number) => void} opts.resume
33
+ * @param {() => (void | null)} opts.abort
34
+ * @param {string} [opts.contentType = '']
35
+ * @param {number} [opts.contentLength]
36
+ * @param {number} [opts.highWaterMark = 64 * 1024]
37
+ */
38
+ constructor({
39
+ resume,
40
+ abort,
41
+ contentType = '',
42
+ contentLength,
43
+ highWaterMark = 64 * 1024, // Same as nodejs fs streams.
44
+ }) {
45
+ super({
46
+ autoDestroy: true,
47
+ read: resume,
48
+ highWaterMark,
49
+ })
50
+
51
+ this._readableState.dataEmitted = false
52
+
53
+ this[kAbort] = abort
54
+
55
+ /**
56
+ * @type {Consume | null}
57
+ */
58
+ this[kConsume] = null
59
+ this[kBytesRead] = 0
60
+ /**
61
+ * @type {ReadableStream|null}
62
+ */
63
+ this[kBody] = null
64
+ this[kUsed] = false
65
+ this[kContentType] = contentType
66
+ this[kContentLength] = Number.isFinite(contentLength) ? contentLength : null
67
+
68
+ // Is stream being consumed through Readable API?
69
+ // This is an optimization so that we avoid checking
70
+ // for 'data' and 'readable' listeners in the hot path
71
+ // inside push().
72
+ this[kReading] = false
73
+ }
74
+
75
+ /**
76
+ * @param {Error|null} err
77
+ * @param {(error:(Error|null)) => void} callback
78
+ * @returns {void}
79
+ */
80
+ _destroy(err, callback) {
81
+ if (!err && !this._readableState.endEmitted) {
82
+ err = new RequestAbortedError()
83
+ }
84
+
85
+ if (err) {
86
+ this[kAbort]()
87
+ }
88
+
89
+ // Workaround for Node "bug". If the stream is destroyed in same
90
+ // tick as it is created, then a user who is waiting for a
91
+ // promise (i.e micro tick) for installing an 'error' listener will
92
+ // never get a chance and will always encounter an unhandled exception.
93
+ if (!this[kUsed]) {
94
+ setImmediate(() => {
95
+ callback(err)
96
+ })
97
+ } else {
98
+ callback(err)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {string} event
104
+ * @param {(...args: any[]) => void} listener
105
+ * @returns {this}
106
+ */
107
+ on(event, listener) {
108
+ if (event === 'data' || event === 'readable') {
109
+ this[kReading] = true
110
+ this[kUsed] = true
111
+ }
112
+ return super.on(event, listener)
113
+ }
114
+
115
+ /**
116
+ * @param {string} event
117
+ * @param {(...args: any[]) => void} listener
118
+ * @returns {this}
119
+ */
120
+ addListener(event, listener) {
121
+ return this.on(event, listener)
122
+ }
123
+
124
+ /**
125
+ * @param {string|symbol} event
126
+ * @param {(...args: any[]) => void} listener
127
+ * @returns {this}
128
+ */
129
+ off(event, listener) {
130
+ const ret = super.off(event, listener)
131
+ if (event === 'data' || event === 'readable') {
132
+ this[kReading] = this.listenerCount('data') > 0 || this.listenerCount('readable') > 0
133
+ }
134
+ return ret
135
+ }
136
+
137
+ /**
138
+ * @param {string|symbol} event
139
+ * @param {(...args: any[]) => void} listener
140
+ * @returns {this}
141
+ */
142
+ removeListener(event, listener) {
143
+ return this.off(event, listener)
144
+ }
145
+
146
+ /**
147
+ * @param {Buffer|null} chunk
148
+ * @returns {boolean}
149
+ */
150
+ push(chunk) {
151
+ this[kBytesRead] += chunk ? chunk.length : 0
152
+
153
+ if (this[kConsume] && chunk !== null) {
154
+ consumePush(this[kConsume], chunk)
155
+ return this[kReading] ? super.push(chunk) : true
156
+ }
157
+ return super.push(chunk)
158
+ }
159
+
160
+ /**
161
+ * Consumes and returns the body as a string.
162
+ *
163
+ * @see https://fetch.spec.whatwg.org/#dom-body-text
164
+ * @returns {Promise<string>}
165
+ */
166
+ text() {
167
+ return consume(this, 'text')
168
+ }
169
+
170
+ /**
171
+ * Consumes and returns the body as a JavaScript Object.
172
+ *
173
+ * @see https://fetch.spec.whatwg.org/#dom-body-json
174
+ * @returns {Promise<unknown>}
175
+ */
176
+ json() {
177
+ return consume(this, 'json')
178
+ }
179
+
180
+ /**
181
+ * Consumes and returns the body as a Blob
182
+ *
183
+ * @see https://fetch.spec.whatwg.org/#dom-body-blob
184
+ * @returns {Promise<Blob>}
185
+ */
186
+ blob() {
187
+ return consume(this, 'blob')
188
+ }
189
+
190
+ /**
191
+ * Consumes and returns the body as an Uint8Array.
192
+ *
193
+ * @see https://fetch.spec.whatwg.org/#dom-body-bytes
194
+ * @returns {Promise<Uint8Array>}
195
+ */
196
+ bytes() {
197
+ return consume(this, 'bytes')
198
+ }
199
+
200
+ /**
201
+ * Consumes and returns the body as an ArrayBuffer.
202
+ *
203
+ * @see https://fetch.spec.whatwg.org/#dom-body-arraybuffer
204
+ * @returns {Promise<ArrayBuffer>}
205
+ */
206
+ arrayBuffer() {
207
+ return consume(this, 'arrayBuffer')
208
+ }
209
+
210
+ /**
211
+ * Not implemented
212
+ *
213
+ * @see https://fetch.spec.whatwg.org/#dom-body-formdata
214
+ * @throws {NotSupportedError}
215
+ */
216
+ async formData() {
217
+ // TODO: Implement.
218
+ throw new NotSupportedError()
219
+ }
220
+
221
+ /**
222
+ * Returns true if the body is not null and the body has been consumed.
223
+ * Otherwise, returns false.
224
+ *
225
+ * @see https://fetch.spec.whatwg.org/#dom-body-bodyused
226
+ * @readonly
227
+ * @returns {boolean}
228
+ */
229
+ get bodyUsed() {
230
+ return isDisturbed(this)
231
+ }
232
+
233
+ /**
234
+ * Dumps the response body by reading `limit` number of bytes.
235
+ * @param {object} opts
236
+ * @param {number} [opts.limit = 131072] Number of bytes to read.
237
+ * @param {AbortSignal} [opts.signal] An AbortSignal to cancel the dump.
238
+ * @returns {Promise<null>}
239
+ */
240
+ async dump(opts) {
241
+ const signal = opts?.signal
242
+
243
+ if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
244
+ throw new InvalidArgumentError('signal must be an AbortSignal')
245
+ }
246
+
247
+ const limit = opts?.limit && Number.isFinite(opts.limit) ? opts.limit : 128 * 1024
248
+
249
+ signal?.throwIfAborted()
250
+
251
+ if (this._readableState.closeEmitted) {
252
+ return null
253
+ }
254
+
255
+ return await new Promise((resolve, reject) => {
256
+ if ((this[kContentLength] && this[kContentLength] > limit) || this[kBytesRead] > limit) {
257
+ this.destroy(new AbortError())
258
+ }
259
+
260
+ if (signal) {
261
+ const onAbort = () => {
262
+ this.destroy(signal.reason ?? new AbortError())
263
+ }
264
+ signal.addEventListener('abort', onAbort)
265
+ this.on('close', function () {
266
+ signal.removeEventListener('abort', onAbort)
267
+ if (signal.aborted) {
268
+ reject(signal.reason ?? new AbortError())
269
+ } else {
270
+ resolve(null)
271
+ }
272
+ })
273
+ } else {
274
+ this.on('close', resolve)
275
+ }
276
+
277
+ this.on('error', noop)
278
+ .on('data', () => {
279
+ if (this[kBytesRead] > limit) {
280
+ this.destroy()
281
+ }
282
+ })
283
+ .resume()
284
+ })
285
+ }
286
+
287
+ /**
288
+ * @param {BufferEncoding} encoding
289
+ * @returns {this}
290
+ */
291
+ setEncoding(encoding) {
292
+ if (Buffer.isEncoding(encoding)) {
293
+ this._readableState.encoding = encoding
294
+ }
295
+ return this
296
+ }
297
+ }
298
+
299
+ /**
300
+ * @see https://streams.spec.whatwg.org/#readablestream-locked
301
+ * @param {BodyReadable} bodyReadable
302
+ * @returns {boolean}
303
+ */
304
+ function isLocked(bodyReadable) {
305
+ // Consume is an implicit lock.
306
+ return bodyReadable[kBody]?.locked === true || bodyReadable[kConsume] !== null
307
+ }
308
+
309
+ /**
310
+ * @see https://fetch.spec.whatwg.org/#body-unusable
311
+ * @param {BodyReadable} bodyReadable
312
+ * @returns {boolean}
313
+ */
314
+ function isUnusable(bodyReadable) {
315
+ return isDisturbed(bodyReadable) || isLocked(bodyReadable)
316
+ }
317
+
318
+ /**
319
+ * @typedef {object} Consume
320
+ * @property {string} type
321
+ * @property {BodyReadable} stream
322
+ * @property {((value?: any) => void)} resolve
323
+ * @property {((err: Error) => void)} reject
324
+ * @property {number} length
325
+ * @property {Buffer[]} body
326
+ */
327
+
328
+ /**
329
+ * @param {BodyReadable} stream
330
+ * @param {string} type
331
+ * @returns {Promise<any>}
332
+ */
333
+ function consume(stream, type) {
334
+ assert(!stream[kConsume])
335
+
336
+ return new Promise((resolve, reject) => {
337
+ if (isUnusable(stream)) {
338
+ const rState = stream._readableState
339
+ if (rState.destroyed && rState.closeEmitted === false) {
340
+ stream
341
+ .on('error', (err) => {
342
+ reject(err)
343
+ })
344
+ .on('close', () => {
345
+ reject(new TypeError('unusable'))
346
+ })
347
+ } else {
348
+ reject(rState.errored ?? new TypeError('unusable'))
349
+ }
350
+ } else {
351
+ queueMicrotask(() => {
352
+ stream[kConsume] = {
353
+ type,
354
+ stream,
355
+ resolve,
356
+ reject,
357
+ length: 0,
358
+ body: [],
359
+ }
360
+
361
+ stream
362
+ .on('error', function (err) {
363
+ consumeFinish(this[kConsume], err)
364
+ })
365
+ .on('close', function () {
366
+ if (this[kConsume].body !== null) {
367
+ consumeFinish(this[kConsume], new RequestAbortedError())
368
+ }
369
+ })
370
+
371
+ consumeStart(stream[kConsume])
372
+ })
373
+ }
374
+ })
375
+ }
376
+
377
+ /**
378
+ * @param {Consume} consume
379
+ * @returns {void}
380
+ */
381
+ function consumeStart(consume) {
382
+ if (consume.body === null) {
383
+ return
384
+ }
385
+
386
+ const { _readableState: state } = consume.stream
387
+
388
+ if (state.bufferIndex) {
389
+ const start = state.bufferIndex
390
+ const end = state.buffer.length
391
+ for (let n = start; n < end; n++) {
392
+ consumePush(consume, state.buffer[n])
393
+ }
394
+ } else {
395
+ for (const chunk of state.buffer) {
396
+ consumePush(consume, chunk)
397
+ }
398
+ }
399
+
400
+ if (state.endEmitted) {
401
+ consumeEnd(this[kConsume], this._readableState.encoding)
402
+ } else {
403
+ consume.stream.on('end', function () {
404
+ consumeEnd(this[kConsume], this._readableState.encoding)
405
+ })
406
+ }
407
+
408
+ consume.stream.resume()
409
+
410
+ while (consume.stream.read() != null) {
411
+ // Loop
412
+ }
413
+ }
414
+
415
+ /**
416
+ * @param {Buffer[]} chunks
417
+ * @param {number} length
418
+ * @param {BufferEncoding} encoding
419
+ * @returns {string}
420
+ */
421
+ function chunksDecode(chunks, length, encoding) {
422
+ if (chunks.length === 0 || length === 0) {
423
+ return ''
424
+ }
425
+ const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length)
426
+ const bufferLength = buffer.length
427
+
428
+ // Skip BOM.
429
+ const start =
430
+ bufferLength > 2 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf ? 3 : 0
431
+ if (!encoding || encoding === 'utf8' || encoding === 'utf-8') {
432
+ return buffer.utf8Slice(start, bufferLength)
433
+ } else {
434
+ return buffer.subarray(start, bufferLength).toString(encoding)
435
+ }
436
+ }
437
+
438
+ /**
439
+ * @param {Buffer[]} chunks
440
+ * @param {number} length
441
+ * @returns {Uint8Array}
442
+ */
443
+ function chunksConcat(chunks, length) {
444
+ if (chunks.length === 0 || length === 0) {
445
+ return new Uint8Array(0)
446
+ }
447
+ if (chunks.length === 1) {
448
+ // fast-path
449
+ return new Uint8Array(chunks[0])
450
+ }
451
+ const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer)
452
+
453
+ let offset = 0
454
+ for (let i = 0; i < chunks.length; ++i) {
455
+ const chunk = chunks[i]
456
+ buffer.set(chunk, offset)
457
+ offset += chunk.length
458
+ }
459
+
460
+ return buffer
461
+ }
462
+
463
+ /**
464
+ * @param {Consume} consume
465
+ * @param {BufferEncoding} encoding
466
+ * @returns {void}
467
+ */
468
+ function consumeEnd(consume, encoding) {
469
+ const { type, body, resolve, stream, length } = consume
470
+
471
+ try {
472
+ if (type === 'text') {
473
+ resolve(chunksDecode(body, length, encoding))
474
+ } else if (type === 'json') {
475
+ resolve(JSON.parse(chunksDecode(body, length, encoding)))
476
+ } else if (type === 'arrayBuffer') {
477
+ resolve(chunksConcat(body, length).buffer)
478
+ } else if (type === 'blob') {
479
+ resolve(new Blob(body, { type: stream[kContentType] }))
480
+ } else if (type === 'bytes') {
481
+ resolve(chunksConcat(body, length))
482
+ }
483
+
484
+ consumeFinish(consume)
485
+ } catch (err) {
486
+ stream.destroy(err)
487
+ }
488
+ }
489
+
490
+ /**
491
+ * @param {Consume} consume
492
+ * @param {Buffer} chunk
493
+ * @returns {void}
494
+ */
495
+ function consumePush(consume, chunk) {
496
+ consume.length += chunk.length
497
+ consume.body.push(chunk)
498
+ }
499
+
500
+ /**
501
+ * @param {Consume} consume
502
+ * @param {Error} [err]
503
+ * @returns {void}
504
+ */
505
+ function consumeFinish(consume, err) {
506
+ if (consume.body === null) {
507
+ return
508
+ }
509
+
510
+ if (err) {
511
+ consume.reject(err)
512
+ } else {
513
+ consume.resolve()
514
+ }
515
+
516
+ // Reset the consume object to allow for garbage collection.
517
+ consume.type = null
518
+ consume.stream = null
519
+ consume.resolve = null
520
+ consume.reject = null
521
+ consume.length = 0
522
+ consume.body = null
523
+ }