mbot/node_modules/undici/lib/web/fetch/body.js
2024-05-10 22:39:11 -07:00

492 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict'
const util = require('../../core/util')
const {
ReadableStreamFrom,
isBlobLike,
isReadableStreamLike,
readableStreamClose,
createDeferredPromise,
fullyReadBody,
extractMimeType,
utf8DecodeBytes
} = require('./util')
const { FormData } = require('./formdata')
const { kState } = require('./symbols')
const { webidl } = require('./webidl')
const { Blob } = require('node:buffer')
const assert = require('node:assert')
const { isErrored } = require('../../core/util')
const { isArrayBuffer } = require('node:util/types')
const { serializeAMimeType } = require('./data-url')
const { multipartFormDataParser } = require('./formdata-parser')
const textEncoder = new TextEncoder()
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) {
// 1. Let stream be null.
let stream = null
// 2. If object is a ReadableStream object, then set stream to object.
if (object instanceof ReadableStream) {
stream = object
} else if (isBlobLike(object)) {
// 3. Otherwise, if object is a Blob object, set stream to the
// result of running objects get stream.
stream = object.stream()
} else {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream with byte reading support.
stream = new ReadableStream({
async pull (controller) {
const buffer = typeof source === 'string' ? textEncoder.encode(source) : source
if (buffer.byteLength) {
controller.enqueue(buffer)
}
queueMicrotask(() => readableStreamClose(controller))
},
start () {},
type: 'bytes'
})
}
// 5. Assert: stream is a ReadableStream object.
assert(isReadableStreamLike(stream))
// 6. Let action be null.
let action = null
// 7. Let source be null.
let source = null
// 8. Let length be null.
let length = null
// 9. Let type be null.
let type = null
// 10. Switch on object:
if (typeof object === 'string') {
// Set source to the UTF-8 encoding of object.
// Note: setting source to a Uint8Array here breaks some mocking assumptions.
source = object
// Set type to `text/plain;charset=UTF-8`.
type = 'text/plain;charset=UTF-8'
} else if (object instanceof URLSearchParams) {
// URLSearchParams
// spec says to run application/x-www-form-urlencoded on body.list
// this is implemented in Node.js as apart of an URLSearchParams instance toString method
// See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
// and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
// Set source to the result of running the application/x-www-form-urlencoded serializer with objects list.
source = object.toString()
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (isArrayBuffer(object)) {
// BufferSource/ArrayBuffer
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.slice())
} else if (ArrayBuffer.isView(object)) {
// BufferSource/ArrayBufferView
// Set source to a copy of the bytes held by object.
source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
} else if (util.isFormDataLike(object)) {
const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}`
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
/*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
const escape = (str) =>
str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
// Set action to this step: run the multipart/form-data
// encoding algorithm, with objects entry list and UTF-8.
// - This ensures that the body is immutable and can't be changed afterwords
// - That the content-length is calculated in advance.
// - And that all parts are pre-encoded and ready to be sent.
const blobParts = []
const rn = new Uint8Array([13, 10]) // '\r\n'
length = 0
let hasUnknownSizeValue = false
for (const [name, value] of object) {
if (typeof value === 'string') {
const chunk = textEncoder.encode(prefix +
`; name="${escape(normalizeLinefeeds(name))}"` +
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
blobParts.push(chunk)
length += chunk.byteLength
} else {
const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
(value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
`Content-Type: ${
value.type || 'application/octet-stream'
}\r\n\r\n`)
blobParts.push(chunk, value, rn)
if (typeof value.size === 'number') {
length += chunk.byteLength + value.size + rn.byteLength
} else {
hasUnknownSizeValue = true
}
}
}
const chunk = textEncoder.encode(`--${boundary}--`)
blobParts.push(chunk)
length += chunk.byteLength
if (hasUnknownSizeValue) {
length = null
}
// Set source to object.
source = object
action = async function * () {
for (const part of blobParts) {
if (part.stream) {
yield * part.stream()
} else {
yield part
}
}
}
// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm.
type = `multipart/form-data; boundary=${boundary}`
} else if (isBlobLike(object)) {
// Blob
// Set source to object.
source = object
// Set length to objects size.
length = object.size
// If objects type attribute is not the empty byte sequence, set
// type to its value.
if (object.type) {
type = object.type
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
if (keepalive) {
throw new TypeError('keepalive')
}
// If object is disturbed or locked, then throw a TypeError.
if (util.isDisturbed(object) || object.locked) {
throw new TypeError(
'Response body object should not be disturbed or locked'
)
}
stream =
object instanceof ReadableStream ? object : ReadableStreamFrom(object)
}
// 11. If source is a byte sequence, then set action to a
// step that returns source and length to sources length.
if (typeof source === 'string' || util.isBuffer(source)) {
length = Buffer.byteLength(source)
}
// 12. If action is non-null, then run these steps in in parallel:
if (action != null) {
// Run action.
let iterator
stream = new ReadableStream({
async start () {
iterator = action(object)[Symbol.asyncIterator]()
},
async pull (controller) {
const { value, done } = await iterator.next()
if (done) {
// When running action is done, close stream.
queueMicrotask(() => {
controller.close()
controller.byobRequest?.respond(0)
})
} else {
// Whenever one or more bytes are available and stream is not errored,
// enqueue a Uint8Array wrapping an ArrayBuffer containing the available
// bytes into stream.
if (!isErrored(stream)) {
const buffer = new Uint8Array(value)
if (buffer.byteLength) {
controller.enqueue(buffer)
}
}
}
return controller.desiredSize > 0
},
async cancel (reason) {
await iterator.return()
},
type: 'bytes'
})
}
// 13. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }
// 14. Return (body, type).
return [body, type]
}
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
function safelyExtractBody (object, keepalive = false) {
// To safely extract a body and a `Content-Type` value from
// a byte sequence or BodyInit object object, run these steps:
// 1. If object is a ReadableStream object, then:
if (object instanceof ReadableStream) {
// Assert: object is neither disturbed nor locked.
// istanbul ignore next
assert(!util.isDisturbed(object), 'The body has already been consumed.')
// istanbul ignore next
assert(!object.locked, 'The stream is locked.')
}
// 2. Return the results of extracting object.
return extractBody(object, keepalive)
}
function cloneBody (body) {
// To clone a body body, run these steps:
// https://fetch.spec.whatwg.org/#concept-body-clone
// 1. Let « out1, out2 » be the result of teeing bodys stream.
const [out1, out2] = body.stream.tee()
// 2. Set bodys stream to out1.
body.stream = out1
// 3. Return a body whose stream is out2 and other members are copied from body.
return {
stream: out2,
length: body.length,
source: body.source
}
}
function throwIfAborted (state) {
if (state.aborted) {
throw new DOMException('The operation was aborted.', 'AbortError')
}
}
function bodyMixinMethods (instance) {
const methods = {
blob () {
// The blob() method steps are to return the result of
// running consume body with this and the following step
// given a byte sequence bytes: return a Blob whose
// contents are bytes and whose type attribute is thiss
// MIME type.
return consumeBody(this, (bytes) => {
let mimeType = bodyMimeType(this)
if (mimeType === null) {
mimeType = ''
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
}
// Return a Blob whose contents are bytes and type attribute
// is mimeType.
return new Blob([bytes], { type: mimeType })
}, instance)
},
arrayBuffer () {
// The arrayBuffer() method steps are to return the result
// of running consume body with this and the following step
// given a byte sequence bytes: return a new ArrayBuffer
// whose contents are bytes.
return consumeBody(this, (bytes) => {
return new Uint8Array(bytes).buffer
}, instance)
},
text () {
// The text() method steps are to return the result of running
// consume body with this and UTF-8 decode.
return consumeBody(this, utf8DecodeBytes, instance)
},
json () {
// The json() method steps are to return the result of running
// consume body with this and parse JSON from bytes.
return consumeBody(this, parseJSONFromBytes, instance)
},
formData () {
// The formData() method steps are to return the result of running
// consume body with this and the following step given a byte sequence bytes:
return consumeBody(this, (value) => {
// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(this)
// 2. If mimeType is non-null, then switch on mimeTypes essence and run
// the corresponding steps:
if (mimeType !== null) {
switch (mimeType.essence) {
case 'multipart/form-data': {
// 1. ... [long step]
const parsed = multipartFormDataParser(value, mimeType)
// 2. If that fails for some reason, then throw a TypeError.
if (parsed === 'failure') {
throw new TypeError('Failed to parse body as FormData.')
}
// 3. Return a new FormData object, appending each entry,
// resulting from the parsing operation, to its entry list.
const fd = new FormData()
fd[kState] = parsed
return fd
}
case 'application/x-www-form-urlencoded': {
// 1. Let entries be the result of parsing bytes.
const entries = new URLSearchParams(value.toString())
// 2. If entries is failure, then throw a TypeError.
// 3. Return a new FormData object whose entry list is entries.
const fd = new FormData()
for (const [name, value] of entries) {
fd.append(name, value)
}
return fd
}
}
}
// 3. Throw a TypeError.
throw new TypeError(
'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
)
}, instance)
}
}
return methods
}
function mixinBody (prototype) {
Object.assign(prototype.prototype, bodyMixinMethods(prototype))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-consume-body
* @param {Response|Request} object
* @param {(value: unknown) => unknown} convertBytesToJSValue
* @param {Response|Request} instance
*/
async function consumeBody (object, convertBytesToJSValue, instance) {
webidl.brandCheck(object, instance)
// 1. If object is unusable, then return a promise rejected
// with a TypeError.
if (bodyUnusable(object[kState].body)) {
throw new TypeError('Body is unusable')
}
throwIfAborted(object[kState])
// 2. Let promise be a new promise.
const promise = createDeferredPromise()
// 3. Let errorSteps given error be to reject promise with error.
const errorSteps = (error) => promise.reject(error)
// 4. Let successSteps given a byte sequence data be to resolve
// promise with the result of running convertBytesToJSValue
// with data. If that threw an exception, then run errorSteps
// with that exception.
const successSteps = (data) => {
try {
promise.resolve(convertBytesToJSValue(data))
} catch (e) {
errorSteps(e)
}
}
// 5. If objects body is null, then run successSteps with an
// empty byte sequence.
if (object[kState].body == null) {
successSteps(new Uint8Array())
return promise.promise
}
// 6. Otherwise, fully read objects body given successSteps,
// errorSteps, and objects relevant global object.
await fullyReadBody(object[kState].body, successSteps, errorSteps)
// 7. Return promise.
return promise.promise
}
// https://fetch.spec.whatwg.org/#body-unusable
function bodyUnusable (body) {
// An object including the Body interface mixin is
// said to be unusable if its body is non-null and
// its bodys stream is disturbed or locked.
return body != null && (body.stream.locked || util.isDisturbed(body.stream))
}
/**
* @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
* @param {Uint8Array} bytes
*/
function parseJSONFromBytes (bytes) {
return JSON.parse(utf8DecodeBytes(bytes))
}
/**
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
* @param {import('./response').Response|import('./request').Request} requestOrResponse
*/
function bodyMimeType (requestOrResponse) {
// 1. Let headers be null.
// 2. If requestOrResponse is a Request object, then set headers to requestOrResponses requests header list.
// 3. Otherwise, set headers to requestOrResponses responses header list.
/** @type {import('./headers').HeadersList} */
const headers = requestOrResponse[kState].headersList
// 4. Let mimeType be the result of extracting a MIME type from headers.
const mimeType = extractMimeType(headers)
// 5. If mimeType is failure, then return null.
if (mimeType === 'failure') {
return null
}
// 6. Return mimeType.
return mimeType
}
module.exports = {
extractBody,
safelyExtractBody,
cloneBody,
mixinBody
}