Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
body.js530 linesDownload Raw Back to fetch
1'use strict'
2
3const util = require('../../core/util')
4const {
5  ReadableStreamFrom,
6  isBlobLike,
7  isReadableStreamLike,
8  readableStreamClose,
9  createDeferredPromise,
10  fullyReadBody,
11  extractMimeType,
12  utf8DecodeBytes
13} = require('./util')
14const { FormData } = require('./formdata')
15const { kState } = require('./symbols')
16const { webidl } = require('./webidl')
17const { Blob } = require('node:buffer')
18const assert = require('node:assert')
19const { isErrored, isDisturbed } = require('node:stream')
20const { isArrayBuffer } = require('node:util/types')
21const { serializeAMimeType } = require('./data-url')
22const { multipartFormDataParser } = require('./formdata-parser')
23let random
24
25try {
26  const crypto = require('node:crypto')
27  random = (max) => crypto.randomInt(0, max)
28} catch {
29  random = (max) => Math.floor(Math.random(max))
30}
31
32const textEncoder = new TextEncoder()
33function noop () {}
34
35const hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf('v18') !== 0
36let streamRegistry
37
38if (hasFinalizationRegistry) {
39  streamRegistry = new FinalizationRegistry((weakRef) => {
40    const stream = weakRef.deref()
41    if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
42      stream.cancel('Response object has been garbage collected').catch(noop)
43    }
44  })
45}
46
47// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
48function extractBody (object, keepalive = false) {
49  // 1. Let stream be null.
50  let stream = null
51
52  // 2. If object is a ReadableStream object, then set stream to object.
53  if (object instanceof ReadableStream) {
54    stream = object
55  } else if (isBlobLike(object)) {
56    // 3. Otherwise, if object is a Blob object, set stream to the
57    //    result of running object’s get stream.
58    stream = object.stream()
59  } else {
60    // 4. Otherwise, set stream to a new ReadableStream object, and set
61    //    up stream with byte reading support.
62    stream = new ReadableStream({
63      async pull (controller) {
64        const buffer = typeof source === 'string' ? textEncoder.encode(source) : source
65
66        if (buffer.byteLength) {
67          controller.enqueue(buffer)
68        }
69
70        queueMicrotask(() => readableStreamClose(controller))
71      },
72      start () {},
73      type: 'bytes'
74    })
75  }
76
77  // 5. Assert: stream is a ReadableStream object.
78  assert(isReadableStreamLike(stream))
79
80  // 6. Let action be null.
81  let action = null
82
83  // 7. Let source be null.
84  let source = null
85
86  // 8. Let length be null.
87  let length = null
88
89  // 9. Let type be null.
90  let type = null
91
92  // 10. Switch on object:
93  if (typeof object === 'string') {
94    // Set source to the UTF-8 encoding of object.
95    // Note: setting source to a Uint8Array here breaks some mocking assumptions.
96    source = object
97
98    // Set type to `text/plain;charset=UTF-8`.
99    type = 'text/plain;charset=UTF-8'
100  } else if (object instanceof URLSearchParams) {
101    // URLSearchParams
102
103    // spec says to run application/x-www-form-urlencoded on body.list
104    // this is implemented in Node.js as apart of an URLSearchParams instance toString method
105    // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
106    // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
107
108    // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
109    source = object.toString()
110
111    // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
112    type = 'application/x-www-form-urlencoded;charset=UTF-8'
113  } else if (isArrayBuffer(object)) {
114    // BufferSource/ArrayBuffer
115
116    // Set source to a copy of the bytes held by object.
117    source = new Uint8Array(object.slice())
118  } else if (ArrayBuffer.isView(object)) {
119    // BufferSource/ArrayBufferView
120
121    // Set source to a copy of the bytes held by object.
122    source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
123  } else if (util.isFormDataLike(object)) {
124    const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
125    const prefix = `--${boundary}\r\nContent-Disposition: form-data`
126
127    /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
128    const escape = (str) =>
129      str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
130    const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
131
132    // Set action to this step: run the multipart/form-data
133    // encoding algorithm, with object’s entry list and UTF-8.
134    // - This ensures that the body is immutable and can't be changed afterwords
135    // - That the content-length is calculated in advance.
136    // - And that all parts are pre-encoded and ready to be sent.
137
138    const blobParts = []
139    const rn = new Uint8Array([13, 10]) // '\r\n'
140    length = 0
141    let hasUnknownSizeValue = false
142
143    for (const [name, value] of object) {
144      if (typeof value === 'string') {
145        const chunk = textEncoder.encode(prefix +
146          `; name="${escape(normalizeLinefeeds(name))}"` +
147          `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
148        blobParts.push(chunk)
149        length += chunk.byteLength
150      } else {
151        const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
152          (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
153          `Content-Type: ${
154            value.type || 'application/octet-stream'
155          }\r\n\r\n`)
156        blobParts.push(chunk, value, rn)
157        if (typeof value.size === 'number') {
158          length += chunk.byteLength + value.size + rn.byteLength
159        } else {
160          hasUnknownSizeValue = true
161        }
162      }
163    }
164
165    // CRLF is appended to the body to function with legacy servers and match other implementations.
166    // https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030
167    // https://github.com/form-data/form-data/issues/63
168    const chunk = textEncoder.encode(`--${boundary}--\r\n`)
169    blobParts.push(chunk)
170    length += chunk.byteLength
171    if (hasUnknownSizeValue) {
172      length = null
173    }
174
175    // Set source to object.
176    source = object
177
178    action = async function * () {
179      for (const part of blobParts) {
180        if (part.stream) {
181          yield * part.stream()
182        } else {
183          yield part
184        }
185      }
186    }
187
188    // Set type to `multipart/form-data; boundary=`,
189    // followed by the multipart/form-data boundary string generated
190    // by the multipart/form-data encoding algorithm.
191    type = `multipart/form-data; boundary=${boundary}`
192  } else if (isBlobLike(object)) {
193    // Blob
194
195    // Set source to object.
196    source = object
197
198    // Set length to object’s size.
199    length = object.size
200
201    // If object’s type attribute is not the empty byte sequence, set
202    // type to its value.
203    if (object.type) {
204      type = object.type
205    }
206  } else if (typeof object[Symbol.asyncIterator] === 'function') {
207    // If keepalive is true, then throw a TypeError.
208    if (keepalive) {
209      throw new TypeError('keepalive')
210    }
211
212    // If object is disturbed or locked, then throw a TypeError.
213    if (util.isDisturbed(object) || object.locked) {
214      throw new TypeError(
215        'Response body object should not be disturbed or locked'
216      )
217    }
218
219    stream =
220      object instanceof ReadableStream ? object : ReadableStreamFrom(object)
221  }
222
223  // 11. If source is a byte sequence, then set action to a
224  // step that returns source and length to source’s length.
225  if (typeof source === 'string' || util.isBuffer(source)) {
226    length = Buffer.byteLength(source)
227  }
228
229  // 12. If action is non-null, then run these steps in in parallel:
230  if (action != null) {
231    // Run action.
232    let iterator
233    stream = new ReadableStream({
234      async start () {
235        iterator = action(object)[Symbol.asyncIterator]()
236      },
237      async pull (controller) {
238        const { value, done } = await iterator.next()
239        if (done) {
240          // When running action is done, close stream.
241          queueMicrotask(() => {
242            controller.close()
243            controller.byobRequest?.respond(0)
244          })
245        } else {
246          // Whenever one or more bytes are available and stream is not errored,
247          // enqueue a Uint8Array wrapping an ArrayBuffer containing the available
248          // bytes into stream.
249          if (!isErrored(stream)) {
250            const buffer = new Uint8Array(value)
251            if (buffer.byteLength) {
252              controller.enqueue(buffer)
253            }
254          }
255        }
256        return controller.desiredSize > 0
257      },
258      async cancel (reason) {
259        await iterator.return()
260      },
261      type: 'bytes'
262    })
263  }
264
265  // 13. Let body be a body whose stream is stream, source is source,
266  // and length is length.
267  const body = { stream, source, length }
268
269  // 14. Return (body, type).
270  return [body, type]
271}
272
273// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
274function safelyExtractBody (object, keepalive = false) {
275  // To safely extract a body and a `Content-Type` value from
276  // a byte sequence or BodyInit object object, run these steps:
277
278  // 1. If object is a ReadableStream object, then:
279  if (object instanceof ReadableStream) {
280    // Assert: object is neither disturbed nor locked.
281    // istanbul ignore next
282    assert(!util.isDisturbed(object), 'The body has already been consumed.')
283    // istanbul ignore next
284    assert(!object.locked, 'The stream is locked.')
285  }
286
287  // 2. Return the results of extracting object.
288  return extractBody(object, keepalive)
289}
290
291function cloneBody (instance, body) {
292  // To clone a body body, run these steps:
293
294  // https://fetch.spec.whatwg.org/#concept-body-clone
295
296  // 1. Let « out1, out2 » be the result of teeing body’s stream.
297  const [out1, out2] = body.stream.tee()
298
299  // 2. Set body’s stream to out1.
300  body.stream = out1
301
302  // 3. Return a body whose stream is out2 and other members are copied from body.
303  return {
304    stream: out2,
305    length: body.length,
306    source: body.source
307  }
308}
309
310function throwIfAborted (state) {
311  if (state.aborted) {
312    throw new DOMException('The operation was aborted.', 'AbortError')
313  }
314}
315
316function bodyMixinMethods (instance) {
317  const methods = {
318    blob () {
319      // The blob() method steps are to return the result of
320      // running consume body with this and the following step
321      // given a byte sequence bytes: return a Blob whose
322      // contents are bytes and whose type attribute is this’s
323      // MIME type.
324      return consumeBody(this, (bytes) => {
325        let mimeType = bodyMimeType(this)
326
327        if (mimeType === null) {
328          mimeType = ''
329        } else if (mimeType) {
330          mimeType = serializeAMimeType(mimeType)
331        }
332
333        // Return a Blob whose contents are bytes and type attribute
334        // is mimeType.
335        return new Blob([bytes], { type: mimeType })
336      }, instance)
337    },
338
339    arrayBuffer () {
340      // The arrayBuffer() method steps are to return the result
341      // of running consume body with this and the following step
342      // given a byte sequence bytes: return a new ArrayBuffer
343      // whose contents are bytes.
344      return consumeBody(this, (bytes) => {
345        return new Uint8Array(bytes).buffer
346      }, instance)
347    },
348
349    text () {
350      // The text() method steps are to return the result of running
351      // consume body with this and UTF-8 decode.
352      return consumeBody(this, utf8DecodeBytes, instance)
353    },
354
355    json () {
356      // The json() method steps are to return the result of running
357      // consume body with this and parse JSON from bytes.
358      return consumeBody(this, parseJSONFromBytes, instance)
359    },
360
361    formData () {
362      // The formData() method steps are to return the result of running
363      // consume body with this and the following step given a byte sequence bytes:
364      return consumeBody(this, (value) => {
365        // 1. Let mimeType be the result of get the MIME type with this.
366        const mimeType = bodyMimeType(this)
367
368        // 2. If mimeType is non-null, then switch on mimeType’s essence and run
369        //    the corresponding steps:
370        if (mimeType !== null) {
371          switch (mimeType.essence) {
372            case 'multipart/form-data': {
373              // 1. ... [long step]
374              const parsed = multipartFormDataParser(value, mimeType)
375
376              // 2. If that fails for some reason, then throw a TypeError.
377              if (parsed === 'failure') {
378                throw new TypeError('Failed to parse body as FormData.')
379              }
380
381              // 3. Return a new FormData object, appending each entry,
382              //    resulting from the parsing operation, to its entry list.
383              const fd = new FormData()
384              fd[kState] = parsed
385
386              return fd
387            }
388            case 'application/x-www-form-urlencoded': {
389              // 1. Let entries be the result of parsing bytes.
390              const entries = new URLSearchParams(value.toString())
391
392              // 2. If entries is failure, then throw a TypeError.
393
394              // 3. Return a new FormData object whose entry list is entries.
395              const fd = new FormData()
396
397              for (const [name, value] of entries) {
398                fd.append(name, value)
399              }
400
401              return fd
402            }
403          }
404        }
405
406        // 3. Throw a TypeError.
407        throw new TypeError(
408          'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
409        )
410      }, instance)
411    },
412
413    bytes () {
414      // The bytes() method steps are to return the result of running consume body
415      // with this and the following step given a byte sequence bytes: return the
416      // result of creating a Uint8Array from bytes in this’s relevant realm.
417      return consumeBody(this, (bytes) => {
418        return new Uint8Array(bytes)
419      }, instance)
420    }
421  }
422
423  return methods
424}
425
426function mixinBody (prototype) {
427  Object.assign(prototype.prototype, bodyMixinMethods(prototype))
428}
429
430/**
431 * @see https://fetch.spec.whatwg.org/#concept-body-consume-body
432 * @param {Response|Request} object
433 * @param {(value: unknown) => unknown} convertBytesToJSValue
434 * @param {Response|Request} instance
435 */
436async function consumeBody (object, convertBytesToJSValue, instance) {
437  webidl.brandCheck(object, instance)
438
439  // 1. If object is unusable, then return a promise rejected
440  //    with a TypeError.
441  if (bodyUnusable(object)) {
442    throw new TypeError('Body is unusable: Body has already been read')
443  }
444
445  throwIfAborted(object[kState])
446
447  // 2. Let promise be a new promise.
448  const promise = createDeferredPromise()
449
450  // 3. Let errorSteps given error be to reject promise with error.
451  const errorSteps = (error) => promise.reject(error)
452
453  // 4. Let successSteps given a byte sequence data be to resolve
454  //    promise with the result of running convertBytesToJSValue
455  //    with data. If that threw an exception, then run errorSteps
456  //    with that exception.
457  const successSteps = (data) => {
458    try {
459      promise.resolve(convertBytesToJSValue(data))
460    } catch (e) {
461      errorSteps(e)
462    }
463  }
464
465  // 5. If object’s body is null, then run successSteps with an
466  //    empty byte sequence.
467  if (object[kState].body == null) {
468    successSteps(Buffer.allocUnsafe(0))
469    return promise.promise
470  }
471
472  // 6. Otherwise, fully read object’s body given successSteps,
473  //    errorSteps, and object’s relevant global object.
474  await fullyReadBody(object[kState].body, successSteps, errorSteps)
475
476  // 7. Return promise.
477  return promise.promise
478}
479
480// https://fetch.spec.whatwg.org/#body-unusable
481function bodyUnusable (object) {
482  const body = object[kState].body
483
484  // An object including the Body interface mixin is
485  // said to be unusable if its body is non-null and
486  // its body’s stream is disturbed or locked.
487  return body != null && (body.stream.locked || util.isDisturbed(body.stream))
488}
489
490/**
491 * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
492 * @param {Uint8Array} bytes
493 */
494function parseJSONFromBytes (bytes) {
495  return JSON.parse(utf8DecodeBytes(bytes))
496}
497
498/**
499 * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
500 * @param {import('./response').Response|import('./request').Request} requestOrResponse
501 */
502function bodyMimeType (requestOrResponse) {
503  // 1. Let headers be null.
504  // 2. If requestOrResponse is a Request object, then set headers to requestOrResponse’s request’s header list.
505  // 3. Otherwise, set headers to requestOrResponse’s response’s header list.
506  /** @type {import('./headers').HeadersList} */
507  const headers = requestOrResponse[kState].headersList
508
509  // 4. Let mimeType be the result of extracting a MIME type from headers.
510  const mimeType = extractMimeType(headers)
511
512  // 5. If mimeType is failure, then return null.
513  if (mimeType === 'failure') {
514    return null
515  }
516
517  // 6. Return mimeType.
518  return mimeType
519}
520
521module.exports = {
522  extractBody,
523  safelyExtractBody,
524  cloneBody,
525  mixinBody,
526  streamRegistry,
527  hasFinalizationRegistry,
528  bodyUnusable
529}
530 
codekingpro/portable-devtools · Team Ai