Spaces:
Runtime error
Runtime error
const Busboy = require('@fastify/busboy') | |
const util = require('../core/util') | |
const { | |
ReadableStreamFrom, | |
isBlobLike, | |
isReadableStreamLike, | |
readableStreamClose, | |
createDeferredPromise, | |
fullyReadBody | |
} = require('./util') | |
const { FormData } = require('./formdata') | |
const { kState } = require('./symbols') | |
const { webidl } = require('./webidl') | |
const { DOMException, structuredClone } = require('./constants') | |
const { Blob, File: NativeFile } = require('buffer') | |
const { kBodyUsed } = require('../core/symbols') | |
const assert = require('assert') | |
const { isErrored } = require('../core/util') | |
const { isUint8Array, isArrayBuffer } = require('util/types') | |
const { File: UndiciFile } = require('./file') | |
const { parseMIMEType, serializeAMimeType } = require('./dataURL') | |
let ReadableStream = globalThis.ReadableStream | |
/** @type {globalThis['File']} */ | |
const File = NativeFile ?? UndiciFile | |
const textEncoder = new TextEncoder() | |
const textDecoder = new TextDecoder() | |
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract | |
function extractBody (object, keepalive = false) { | |
if (!ReadableStream) { | |
ReadableStream = require('stream/web').ReadableStream | |
} | |
// 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 object’s get stream. | |
stream = object.stream() | |
} else { | |
// 4. Otherwise, set stream to a new ReadableStream object, and set | |
// up stream. | |
stream = new ReadableStream({ | |
async pull (controller) { | |
controller.enqueue( | |
typeof source === 'string' ? textEncoder.encode(source) : source | |
) | |
queueMicrotask(() => readableStreamClose(controller)) | |
}, | |
start () {}, | |
type: undefined | |
}) | |
} | |
// 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 object’s 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 object’s 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 object’s size. | |
length = object.size | |
// If object’s 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 source’s 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() | |
}) | |
} 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)) { | |
controller.enqueue(new Uint8Array(value)) | |
} | |
} | |
return controller.desiredSize > 0 | |
}, | |
async cancel (reason) { | |
await iterator.return() | |
}, | |
type: undefined | |
}) | |
} | |
// 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) { | |
if (!ReadableStream) { | |
// istanbul ignore next | |
ReadableStream = require('stream/web').ReadableStream | |
} | |
// 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 body’s stream. | |
const [out1, out2] = body.stream.tee() | |
const out2Clone = structuredClone(out2, { transfer: [out2] }) | |
// This, for whatever reasons, unrefs out2Clone which allows | |
// the process to exit by itself. | |
const [, finalClone] = out2Clone.tee() | |
// 2. Set body’s stream to out1. | |
body.stream = out1 | |
// 3. Return a body whose stream is out2 and other members are copied from body. | |
return { | |
stream: finalClone, | |
length: body.length, | |
source: body.source | |
} | |
} | |
async function * consumeBody (body) { | |
if (body) { | |
if (isUint8Array(body)) { | |
yield body | |
} else { | |
const stream = body.stream | |
if (util.isDisturbed(stream)) { | |
throw new TypeError('The body has already been consumed.') | |
} | |
if (stream.locked) { | |
throw new TypeError('The stream is locked.') | |
} | |
// Compat. | |
stream[kBodyUsed] = true | |
yield * stream | |
} | |
} | |
} | |
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 this’s | |
// MIME type. | |
return specConsumeBody(this, (bytes) => { | |
let mimeType = bodyMimeType(this) | |
if (mimeType === 'failure') { | |
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 specConsumeBody(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 specConsumeBody(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 specConsumeBody(this, parseJSONFromBytes, instance) | |
}, | |
async formData () { | |
webidl.brandCheck(this, instance) | |
throwIfAborted(this[kState]) | |
const contentType = this.headers.get('Content-Type') | |
// If mimeType’s essence is "multipart/form-data", then: | |
if (/multipart\/form-data/.test(contentType)) { | |
const headers = {} | |
for (const [key, value] of this.headers) headers[key.toLowerCase()] = value | |
const responseFormData = new FormData() | |
let busboy | |
try { | |
busboy = new Busboy({ | |
headers, | |
preservePath: true | |
}) | |
} catch (err) { | |
throw new DOMException(`${err}`, 'AbortError') | |
} | |
busboy.on('field', (name, value) => { | |
responseFormData.append(name, value) | |
}) | |
busboy.on('file', (name, value, filename, encoding, mimeType) => { | |
const chunks = [] | |
if (encoding === 'base64' || encoding.toLowerCase() === 'base64') { | |
let base64chunk = '' | |
value.on('data', (chunk) => { | |
base64chunk += chunk.toString().replace(/[\r\n]/gm, '') | |
const end = base64chunk.length - base64chunk.length % 4 | |
chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) | |
base64chunk = base64chunk.slice(end) | |
}) | |
value.on('end', () => { | |
chunks.push(Buffer.from(base64chunk, 'base64')) | |
responseFormData.append(name, new File(chunks, filename, { type: mimeType })) | |
}) | |
} else { | |
value.on('data', (chunk) => { | |
chunks.push(chunk) | |
}) | |
value.on('end', () => { | |
responseFormData.append(name, new File(chunks, filename, { type: mimeType })) | |
}) | |
} | |
}) | |
const busboyResolve = new Promise((resolve, reject) => { | |
busboy.on('finish', resolve) | |
busboy.on('error', (err) => reject(new TypeError(err))) | |
}) | |
if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) | |
busboy.end() | |
await busboyResolve | |
return responseFormData | |
} else if (/application\/x-www-form-urlencoded/.test(contentType)) { | |
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: | |
// 1. Let entries be the result of parsing bytes. | |
let entries | |
try { | |
let text = '' | |
// application/x-www-form-urlencoded parser will keep the BOM. | |
// https://url.spec.whatwg.org/#concept-urlencoded-parser | |
// Note that streaming decoder is stateful and cannot be reused | |
const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) | |
for await (const chunk of consumeBody(this[kState].body)) { | |
if (!isUint8Array(chunk)) { | |
throw new TypeError('Expected Uint8Array chunk') | |
} | |
text += streamingDecoder.decode(chunk, { stream: true }) | |
} | |
text += streamingDecoder.decode() | |
entries = new URLSearchParams(text) | |
} catch (err) { | |
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string. | |
// 2. If entries is failure, then throw a TypeError. | |
throw Object.assign(new TypeError(), { cause: err }) | |
} | |
// 3. Return a new FormData object whose entries are entries. | |
const formData = new FormData() | |
for (const [name, value] of entries) { | |
formData.append(name, value) | |
} | |
return formData | |
} else { | |
// Wait a tick before checking if the request has been aborted. | |
// Otherwise, a TypeError can be thrown when an AbortError should. | |
await Promise.resolve() | |
throwIfAborted(this[kState]) | |
// Otherwise, throw a TypeError. | |
throw webidl.errors.exception({ | |
header: `${instance.name}.formData`, | |
message: 'Could not parse content as FormData.' | |
}) | |
} | |
} | |
} | |
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 specConsumeBody (object, convertBytesToJSValue, instance) { | |
webidl.brandCheck(object, instance) | |
throwIfAborted(object[kState]) | |
// 1. If object is unusable, then return a promise rejected | |
// with a TypeError. | |
if (bodyUnusable(object[kState].body)) { | |
throw new TypeError('Body is unusable') | |
} | |
// 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 object’s 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 object’s body given successSteps, | |
// errorSteps, and object’s 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 body’s stream is disturbed or locked. | |
return body != null && (body.stream.locked || util.isDisturbed(body.stream)) | |
} | |
/** | |
* @see https://encoding.spec.whatwg.org/#utf-8-decode | |
* @param {Buffer} buffer | |
*/ | |
function utf8DecodeBytes (buffer) { | |
if (buffer.length === 0) { | |
return '' | |
} | |
// 1. Let buffer be the result of peeking three bytes from | |
// ioQueue, converted to a byte sequence. | |
// 2. If buffer is 0xEF 0xBB 0xBF, then read three | |
// bytes from ioQueue. (Do nothing with those bytes.) | |
if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { | |
buffer = buffer.subarray(3) | |
} | |
// 3. Process a queue with an instance of UTF-8’s | |
// decoder, ioQueue, output, and "replacement". | |
const output = textDecoder.decode(buffer) | |
// 4. Return output. | |
return output | |
} | |
/** | |
* @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} object | |
*/ | |
function bodyMimeType (object) { | |
const { headersList } = object[kState] | |
const contentType = headersList.get('content-type') | |
if (contentType === null) { | |
return 'failure' | |
} | |
return parseMIMEType(contentType) | |
} | |
module.exports = { | |
extractBody, | |
safelyExtractBody, | |
cloneBody, | |
mixinBody | |
} | |