• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const Busboy = require('busboy')
4const util = require('../core/util')
5const {
6  ReadableStreamFrom,
7  isBlobLike,
8  isReadableStreamLike,
9  readableStreamClose,
10  createDeferredPromise,
11  fullyReadBody
12} = require('./util')
13const { FormData } = require('./formdata')
14const { kState } = require('./symbols')
15const { webidl } = require('./webidl')
16const { DOMException, structuredClone } = require('./constants')
17const { Blob, File: NativeFile } = require('buffer')
18const { kBodyUsed } = require('../core/symbols')
19const assert = require('assert')
20const { isErrored } = require('../core/util')
21const { isUint8Array, isArrayBuffer } = require('util/types')
22const { File: UndiciFile } = require('./file')
23const { parseMIMEType, serializeAMimeType } = require('./dataURL')
24
25let ReadableStream = globalThis.ReadableStream
26
27/** @type {globalThis['File']} */
28const File = NativeFile ?? UndiciFile
29
30// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
31function extractBody (object, keepalive = false) {
32  if (!ReadableStream) {
33    ReadableStream = require('stream/web').ReadableStream
34  }
35
36  // 1. Let stream be null.
37  let stream = null
38
39  // 2. If object is a ReadableStream object, then set stream to object.
40  if (object instanceof ReadableStream) {
41    stream = object
42  } else if (isBlobLike(object)) {
43    // 3. Otherwise, if object is a Blob object, set stream to the
44    //    result of running object’s get stream.
45    stream = object.stream()
46  } else {
47    // 4. Otherwise, set stream to a new ReadableStream object, and set
48    //    up stream.
49    stream = new ReadableStream({
50      async pull (controller) {
51        controller.enqueue(
52          typeof source === 'string' ? new TextEncoder().encode(source) : source
53        )
54        queueMicrotask(() => readableStreamClose(controller))
55      },
56      start () {},
57      type: undefined
58    })
59  }
60
61  // 5. Assert: stream is a ReadableStream object.
62  assert(isReadableStreamLike(stream))
63
64  // 6. Let action be null.
65  let action = null
66
67  // 7. Let source be null.
68  let source = null
69
70  // 8. Let length be null.
71  let length = null
72
73  // 9. Let type be null.
74  let type = null
75
76  // 10. Switch on object:
77  if (typeof object === 'string') {
78    // Set source to the UTF-8 encoding of object.
79    // Note: setting source to a Uint8Array here breaks some mocking assumptions.
80    source = object
81
82    // Set type to `text/plain;charset=UTF-8`.
83    type = 'text/plain;charset=UTF-8'
84  } else if (object instanceof URLSearchParams) {
85    // URLSearchParams
86
87    // spec says to run application/x-www-form-urlencoded on body.list
88    // this is implemented in Node.js as apart of an URLSearchParams instance toString method
89    // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
90    // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
91
92    // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
93    source = object.toString()
94
95    // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
96    type = 'application/x-www-form-urlencoded;charset=UTF-8'
97  } else if (isArrayBuffer(object)) {
98    // BufferSource/ArrayBuffer
99
100    // Set source to a copy of the bytes held by object.
101    source = new Uint8Array(object.slice())
102  } else if (ArrayBuffer.isView(object)) {
103    // BufferSource/ArrayBufferView
104
105    // Set source to a copy of the bytes held by object.
106    source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
107  } else if (util.isFormDataLike(object)) {
108    const boundary = `----formdata-undici-${Math.random()}`.replace('.', '').slice(0, 32)
109    const prefix = `--${boundary}\r\nContent-Disposition: form-data`
110
111    /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
112    const escape = (str) =>
113      str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
114    const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
115
116    // Set action to this step: run the multipart/form-data
117    // encoding algorithm, with object’s entry list and UTF-8.
118    // - This ensures that the body is immutable and can't be changed afterwords
119    // - That the content-length is calculated in advance.
120    // - And that all parts are pre-encoded and ready to be sent.
121
122    const enc = new TextEncoder()
123    const blobParts = []
124    const rn = new Uint8Array([13, 10]) // '\r\n'
125    length = 0
126    let hasUnknownSizeValue = false
127
128    for (const [name, value] of object) {
129      if (typeof value === 'string') {
130        const chunk = enc.encode(prefix +
131          `; name="${escape(normalizeLinefeeds(name))}"` +
132          `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
133        blobParts.push(chunk)
134        length += chunk.byteLength
135      } else {
136        const chunk = enc.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
137          (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
138          `Content-Type: ${
139            value.type || 'application/octet-stream'
140          }\r\n\r\n`)
141        blobParts.push(chunk, value, rn)
142        if (typeof value.size === 'number') {
143          length += chunk.byteLength + value.size + rn.byteLength
144        } else {
145          hasUnknownSizeValue = true
146        }
147      }
148    }
149
150    const chunk = enc.encode(`--${boundary}--`)
151    blobParts.push(chunk)
152    length += chunk.byteLength
153    if (hasUnknownSizeValue) {
154      length = null
155    }
156
157    // Set source to object.
158    source = object
159
160    action = async function * () {
161      for (const part of blobParts) {
162        if (part.stream) {
163          yield * part.stream()
164        } else {
165          yield part
166        }
167      }
168    }
169
170    // Set type to `multipart/form-data; boundary=`,
171    // followed by the multipart/form-data boundary string generated
172    // by the multipart/form-data encoding algorithm.
173    type = 'multipart/form-data; boundary=' + boundary
174  } else if (isBlobLike(object)) {
175    // Blob
176
177    // Set source to object.
178    source = object
179
180    // Set length to object’s size.
181    length = object.size
182
183    // If object’s type attribute is not the empty byte sequence, set
184    // type to its value.
185    if (object.type) {
186      type = object.type
187    }
188  } else if (typeof object[Symbol.asyncIterator] === 'function') {
189    // If keepalive is true, then throw a TypeError.
190    if (keepalive) {
191      throw new TypeError('keepalive')
192    }
193
194    // If object is disturbed or locked, then throw a TypeError.
195    if (util.isDisturbed(object) || object.locked) {
196      throw new TypeError(
197        'Response body object should not be disturbed or locked'
198      )
199    }
200
201    stream =
202      object instanceof ReadableStream ? object : ReadableStreamFrom(object)
203  }
204
205  // 11. If source is a byte sequence, then set action to a
206  // step that returns source and length to source’s length.
207  if (typeof source === 'string' || util.isBuffer(source)) {
208    length = Buffer.byteLength(source)
209  }
210
211  // 12. If action is non-null, then run these steps in in parallel:
212  if (action != null) {
213    // Run action.
214    let iterator
215    stream = new ReadableStream({
216      async start () {
217        iterator = action(object)[Symbol.asyncIterator]()
218      },
219      async pull (controller) {
220        const { value, done } = await iterator.next()
221        if (done) {
222          // When running action is done, close stream.
223          queueMicrotask(() => {
224            controller.close()
225          })
226        } else {
227          // Whenever one or more bytes are available and stream is not errored,
228          // enqueue a Uint8Array wrapping an ArrayBuffer containing the available
229          // bytes into stream.
230          if (!isErrored(stream)) {
231            controller.enqueue(new Uint8Array(value))
232          }
233        }
234        return controller.desiredSize > 0
235      },
236      async cancel (reason) {
237        await iterator.return()
238      },
239      type: undefined
240    })
241  }
242
243  // 13. Let body be a body whose stream is stream, source is source,
244  // and length is length.
245  const body = { stream, source, length }
246
247  // 14. Return (body, type).
248  return [body, type]
249}
250
251// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
252function safelyExtractBody (object, keepalive = false) {
253  if (!ReadableStream) {
254    // istanbul ignore next
255    ReadableStream = require('stream/web').ReadableStream
256  }
257
258  // To safely extract a body and a `Content-Type` value from
259  // a byte sequence or BodyInit object object, run these steps:
260
261  // 1. If object is a ReadableStream object, then:
262  if (object instanceof ReadableStream) {
263    // Assert: object is neither disturbed nor locked.
264    // istanbul ignore next
265    assert(!util.isDisturbed(object), 'The body has already been consumed.')
266    // istanbul ignore next
267    assert(!object.locked, 'The stream is locked.')
268  }
269
270  // 2. Return the results of extracting object.
271  return extractBody(object, keepalive)
272}
273
274function cloneBody (body) {
275  // To clone a body body, run these steps:
276
277  // https://fetch.spec.whatwg.org/#concept-body-clone
278
279  // 1. Let « out1, out2 » be the result of teeing body’s stream.
280  const [out1, out2] = body.stream.tee()
281  const out2Clone = structuredClone(out2, { transfer: [out2] })
282  // This, for whatever reasons, unrefs out2Clone which allows
283  // the process to exit by itself.
284  const [, finalClone] = out2Clone.tee()
285
286  // 2. Set body’s stream to out1.
287  body.stream = out1
288
289  // 3. Return a body whose stream is out2 and other members are copied from body.
290  return {
291    stream: finalClone,
292    length: body.length,
293    source: body.source
294  }
295}
296
297async function * consumeBody (body) {
298  if (body) {
299    if (isUint8Array(body)) {
300      yield body
301    } else {
302      const stream = body.stream
303
304      if (util.isDisturbed(stream)) {
305        throw new TypeError('The body has already been consumed.')
306      }
307
308      if (stream.locked) {
309        throw new TypeError('The stream is locked.')
310      }
311
312      // Compat.
313      stream[kBodyUsed] = true
314
315      yield * stream
316    }
317  }
318}
319
320function throwIfAborted (state) {
321  if (state.aborted) {
322    throw new DOMException('The operation was aborted.', 'AbortError')
323  }
324}
325
326function bodyMixinMethods (instance) {
327  const methods = {
328    blob () {
329      // The blob() method steps are to return the result of
330      // running consume body with this and the following step
331      // given a byte sequence bytes: return a Blob whose
332      // contents are bytes and whose type attribute is this’s
333      // MIME type.
334      return specConsumeBody(this, (bytes) => {
335        let mimeType = bodyMimeType(this)
336
337        if (mimeType === 'failure') {
338          mimeType = ''
339        } else if (mimeType) {
340          mimeType = serializeAMimeType(mimeType)
341        }
342
343        // Return a Blob whose contents are bytes and type attribute
344        // is mimeType.
345        return new Blob([bytes], { type: mimeType })
346      }, instance)
347    },
348
349    arrayBuffer () {
350      // The arrayBuffer() method steps are to return the result
351      // of running consume body with this and the following step
352      // given a byte sequence bytes: return a new ArrayBuffer
353      // whose contents are bytes.
354      return specConsumeBody(this, (bytes) => {
355        return new Uint8Array(bytes).buffer
356      }, instance)
357    },
358
359    text () {
360      // The text() method steps are to return the result of running
361      // consume body with this and UTF-8 decode.
362      return specConsumeBody(this, utf8DecodeBytes, instance)
363    },
364
365    json () {
366      // The json() method steps are to return the result of running
367      // consume body with this and parse JSON from bytes.
368      return specConsumeBody(this, parseJSONFromBytes, instance)
369    },
370
371    async formData () {
372      webidl.brandCheck(this, instance)
373
374      throwIfAborted(this[kState])
375
376      const contentType = this.headers.get('Content-Type')
377
378      // If mimeType’s essence is "multipart/form-data", then:
379      if (/multipart\/form-data/.test(contentType)) {
380        const headers = {}
381        for (const [key, value] of this.headers) headers[key.toLowerCase()] = value
382
383        const responseFormData = new FormData()
384
385        let busboy
386
387        try {
388          busboy = Busboy({
389            headers,
390            defParamCharset: 'utf8'
391          })
392        } catch (err) {
393          throw new DOMException(`${err}`, 'AbortError')
394        }
395
396        busboy.on('field', (name, value) => {
397          responseFormData.append(name, value)
398        })
399        busboy.on('file', (name, value, info) => {
400          const { filename, encoding, mimeType } = info
401          const chunks = []
402
403          if (encoding === 'base64' || encoding.toLowerCase() === 'base64') {
404            let base64chunk = ''
405
406            value.on('data', (chunk) => {
407              base64chunk += chunk.toString().replace(/[\r\n]/gm, '')
408
409              const end = base64chunk.length - base64chunk.length % 4
410              chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64'))
411
412              base64chunk = base64chunk.slice(end)
413            })
414            value.on('end', () => {
415              chunks.push(Buffer.from(base64chunk, 'base64'))
416              responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
417            })
418          } else {
419            value.on('data', (chunk) => {
420              chunks.push(chunk)
421            })
422            value.on('end', () => {
423              responseFormData.append(name, new File(chunks, filename, { type: mimeType }))
424            })
425          }
426        })
427
428        const busboyResolve = new Promise((resolve, reject) => {
429          busboy.on('finish', resolve)
430          busboy.on('error', (err) => reject(new TypeError(err)))
431        })
432
433        if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
434        busboy.end()
435        await busboyResolve
436
437        return responseFormData
438      } else if (/application\/x-www-form-urlencoded/.test(contentType)) {
439        // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
440
441        // 1. Let entries be the result of parsing bytes.
442        let entries
443        try {
444          let text = ''
445          // application/x-www-form-urlencoded parser will keep the BOM.
446          // https://url.spec.whatwg.org/#concept-urlencoded-parser
447          const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
448          for await (const chunk of consumeBody(this[kState].body)) {
449            if (!isUint8Array(chunk)) {
450              throw new TypeError('Expected Uint8Array chunk')
451            }
452            text += textDecoder.decode(chunk, { stream: true })
453          }
454          text += textDecoder.decode()
455          entries = new URLSearchParams(text)
456        } catch (err) {
457          // istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
458          // 2. If entries is failure, then throw a TypeError.
459          throw Object.assign(new TypeError(), { cause: err })
460        }
461
462        // 3. Return a new FormData object whose entries are entries.
463        const formData = new FormData()
464        for (const [name, value] of entries) {
465          formData.append(name, value)
466        }
467        return formData
468      } else {
469        // Wait a tick before checking if the request has been aborted.
470        // Otherwise, a TypeError can be thrown when an AbortError should.
471        await Promise.resolve()
472
473        throwIfAborted(this[kState])
474
475        // Otherwise, throw a TypeError.
476        throw webidl.errors.exception({
477          header: `${instance.name}.formData`,
478          message: 'Could not parse content as FormData.'
479        })
480      }
481    }
482  }
483
484  return methods
485}
486
487function mixinBody (prototype) {
488  Object.assign(prototype.prototype, bodyMixinMethods(prototype))
489}
490
491/**
492 * @see https://fetch.spec.whatwg.org/#concept-body-consume-body
493 * @param {Response|Request} object
494 * @param {(value: unknown) => unknown} convertBytesToJSValue
495 * @param {Response|Request} instance
496 */
497async function specConsumeBody (object, convertBytesToJSValue, instance) {
498  webidl.brandCheck(object, instance)
499
500  throwIfAborted(object[kState])
501
502  // 1. If object is unusable, then return a promise rejected
503  //    with a TypeError.
504  if (bodyUnusable(object[kState].body)) {
505    throw new TypeError('Body is unusable')
506  }
507
508  // 2. Let promise be a new promise.
509  const promise = createDeferredPromise()
510
511  // 3. Let errorSteps given error be to reject promise with error.
512  const errorSteps = (error) => promise.reject(error)
513
514  // 4. Let successSteps given a byte sequence data be to resolve
515  //    promise with the result of running convertBytesToJSValue
516  //    with data. If that threw an exception, then run errorSteps
517  //    with that exception.
518  const successSteps = (data) => {
519    try {
520      promise.resolve(convertBytesToJSValue(data))
521    } catch (e) {
522      errorSteps(e)
523    }
524  }
525
526  // 5. If object’s body is null, then run successSteps with an
527  //    empty byte sequence.
528  if (object[kState].body == null) {
529    successSteps(new Uint8Array())
530    return promise.promise
531  }
532
533  // 6. Otherwise, fully read object’s body given successSteps,
534  //    errorSteps, and object’s relevant global object.
535  fullyReadBody(object[kState].body, successSteps, errorSteps)
536
537  // 7. Return promise.
538  return promise.promise
539}
540
541// https://fetch.spec.whatwg.org/#body-unusable
542function bodyUnusable (body) {
543  // An object including the Body interface mixin is
544  // said to be unusable if its body is non-null and
545  // its body’s stream is disturbed or locked.
546  return body != null && (body.stream.locked || util.isDisturbed(body.stream))
547}
548
549/**
550 * @see https://encoding.spec.whatwg.org/#utf-8-decode
551 * @param {Buffer} buffer
552 */
553function utf8DecodeBytes (buffer) {
554  if (buffer.length === 0) {
555    return ''
556  }
557
558  // 1. Let buffer be the result of peeking three bytes from
559  //    ioQueue, converted to a byte sequence.
560
561  // 2. If buffer is 0xEF 0xBB 0xBF, then read three
562  //    bytes from ioQueue. (Do nothing with those bytes.)
563  if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
564    buffer = buffer.subarray(3)
565  }
566
567  // 3. Process a queue with an instance of UTF-8’s
568  //    decoder, ioQueue, output, and "replacement".
569  const output = new TextDecoder().decode(buffer)
570
571  // 4. Return output.
572  return output
573}
574
575/**
576 * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
577 * @param {Uint8Array} bytes
578 */
579function parseJSONFromBytes (bytes) {
580  return JSON.parse(utf8DecodeBytes(bytes))
581}
582
583/**
584 * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
585 * @param {import('./response').Response|import('./request').Request} object
586 */
587function bodyMimeType (object) {
588  const { headersList } = object[kState]
589  const contentType = headersList.get('content-type')
590
591  if (contentType === null) {
592    return 'failure'
593  }
594
595  return parseMIMEType(contentType)
596}
597
598module.exports = {
599  extractBody,
600  safelyExtractBody,
601  cloneBody,
602  mixinBody
603}
604