/** * Body.js * * Body interface provides common methods for Request and Response */ import Stream, {PassThrough} from 'node:stream'; import {types, deprecate, promisify} from 'node:util'; import {Buffer} from 'node:buffer'; import Blob from 'fetch-blob'; import {FormData, formDataToBlob} from 'formdata-polyfill/esm.min.js'; import {FetchError} from './errors/fetch-error.js'; import {FetchBaseError} from './errors/base.js'; import {isBlob, isURLSearchParameters} from './utils/is.js'; const pipeline = promisify(Stream.pipeline); const INTERNALS = Symbol('Body internals'); /** * Body mixin * * Ref: https://fetch.spec.whatwg.org/#body * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ export default class Body { constructor(body, { size = 0 } = {}) { let boundary = null; if (body === null) { // Body is undefined or null body = null; } else if (isURLSearchParameters(body)) { // Body is a URLSearchParams body = Buffer.from(body.toString()); } else if (isBlob(body)) { // Body is blob } else if (Buffer.isBuffer(body)) { // Body is Buffer } else if (types.isAnyArrayBuffer(body)) { // Body is ArrayBuffer body = Buffer.from(body); } else if (ArrayBuffer.isView(body)) { // Body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { // Body is stream } else if (body instanceof FormData) { // Body is FormData body = formDataToBlob(body); boundary = body.type.split('=')[1]; } else { // None of the above // coerce to string then buffer body = Buffer.from(String(body)); } let stream = body; if (Buffer.isBuffer(body)) { stream = Stream.Readable.from(body); } else if (isBlob(body)) { stream = Stream.Readable.from(body.stream()); } this[INTERNALS] = { body, stream, boundary, disturbed: false, error: null }; this.size = size; if (body instanceof Stream) { body.on('error', error_ => { const error = error_ instanceof FetchBaseError ? error_ : new FetchError(`Invalid response body while trying to fetch ${this.url}: ${error_.message}`, 'system', error_); this[INTERNALS].error = error; }); } } get body() { return this[INTERNALS].stream; } get bodyUsed() { return this[INTERNALS].disturbed; } /** * Decode response as ArrayBuffer * * @return Promise */ async arrayBuffer() { const {buffer, byteOffset, byteLength} = await consumeBody(this); return buffer.slice(byteOffset, byteOffset + byteLength); } async formData() { const ct = this.headers.get('content-type'); if (ct.startsWith('application/x-www-form-urlencoded')) { const formData = new FormData(); const parameters = new URLSearchParams(await this.text()); for (const [name, value] of parameters) { formData.append(name, value); } return formData; } const {toFormData} = await import('./utils/multipart-parser.js'); return toFormData(this.body, ct); } /** * Return raw response as Blob * * @return Promise */ async blob() { const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || ''; const buf = await this.arrayBuffer(); return new Blob([buf], { type: ct }); } /** * Decode response as json * * @return Promise */ async json() { const text = await this.text(); return JSON.parse(text); } /** * Decode response as text * * @return Promise */ async text() { const buffer = await consumeBody(this); return new TextDecoder().decode(buffer); } /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer() { return consumeBody(this); } } Body.prototype.buffer = deprecate(Body.prototype.buffer, 'Please use \'response.arrayBuffer()\' instead of \'response.buffer()\'', 'node-fetch#buffer'); // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { body: {enumerable: true}, bodyUsed: {enumerable: true}, arrayBuffer: {enumerable: true}, blob: {enumerable: true}, json: {enumerable: true}, text: {enumerable: true}, data: {get: deprecate(() => {}, 'data doesn\'t exist, use json(), text(), arrayBuffer(), or body instead', 'https://github.com/node-fetch/node-fetch/issues/1000 (response)')} }); /** * Consume and convert an entire Body to a Buffer. * * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body * * @return Promise */ async function consumeBody(data) { if (data[INTERNALS].disturbed) { throw new TypeError(`body used already for: ${data.url}`); } data[INTERNALS].disturbed = true; if (data[INTERNALS].error) { throw data[INTERNALS].error; } const {body} = data; // Body is null if (body === null) { return Buffer.alloc(0); } /* c8 ignore next 3 */ if (!(body instanceof Stream)) { return Buffer.alloc(0); } // Body is stream // get ready to actually consume the body const accum = []; let accumBytes = 0; try { for await (const chunk of body) { if (data.size > 0 && accumBytes + chunk.length > data.size) { const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'); body.destroy(error); throw error; } accumBytes += chunk.length; accum.push(chunk); } } catch (error) { const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error); throw error_; } if (body.readableEnded === true || body._readableState.ended === true) { try { if (accum.every(c => typeof c === 'string')) { return Buffer.from(accum.join('')); } return Buffer.concat(accum, accumBytes); } catch (error) { throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error); } } else { throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`); } } /** * Clone body given Res/Req instance * * @param Mixed instance Response or Request instance * @param String highWaterMark highWaterMark for both PassThrough body streams * @return Mixed */ export const clone = (instance, highWaterMark) => { let p1; let p2; let {body} = instance[INTERNALS]; // Don't allow cloning a used body if (instance.bodyUsed) { throw new Error('cannot clone body after it is used'); } // Check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) { // Tee instance body p1 = new PassThrough({highWaterMark}); p2 = new PassThrough({highWaterMark}); body.pipe(p1); body.pipe(p2); // Set instance body to teed body and return the other teed body instance[INTERNALS].stream = p1; body = p2; } return body; }; const getNonSpecFormDataBoundary = deprecate( body => body.getBoundary(), 'form-data doesn\'t follow the spec and requires special treatment. Use alternative package', 'https://github.com/node-fetch/node-fetch/issues/1167' ); /** * Performs the operation "extract a `Content-Type` value from |object|" as * specified in the specification: * https://fetch.spec.whatwg.org/#concept-bodyinit-extract * * This function assumes that instance.body is present. * * @param {any} body Any options.body input * @returns {string | null} */ export const extractContentType = (body, request) => { // Body is null or undefined if (body === null) { return null; } // Body is string if (typeof body === 'string') { return 'text/plain;charset=UTF-8'; } // Body is a URLSearchParams if (isURLSearchParameters(body)) { return 'application/x-www-form-urlencoded;charset=UTF-8'; } // Body is blob if (isBlob(body)) { return body.type || null; } // Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView) if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) { return null; } if (body instanceof FormData) { return `multipart/form-data; boundary=${request[INTERNALS].boundary}`; } // Detect form data input from form-data module if (body && typeof body.getBoundary === 'function') { return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`; } // Body is stream - can't really do much about this if (body instanceof Stream) { return null; } // Body constructor defaults other things to string return 'text/plain;charset=UTF-8'; }; /** * The Fetch Standard treats this as if "total bytes" is a property on the body. * For us, we have to explicitly get it with a function. * * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes * * @param {any} obj.body Body object from the Body instance. * @returns {number | null} */ export const getTotalBytes = request => { const {body} = request[INTERNALS]; // Body is null or undefined if (body === null) { return 0; } // Body is Blob if (isBlob(body)) { return body.size; } // Body is Buffer if (Buffer.isBuffer(body)) { return body.length; } // Detect form data input from form-data module if (body && typeof body.getLengthSync === 'function') { return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null; } // Body is stream return null; }; /** * Write a Body to a Node.js WritableStream (e.g. http.Request) object. * * @param {Stream.Writable} dest The stream to write to. * @param obj.body Body object from the Body instance. * @returns {Promise} */ export const writeToStream = async (dest, {body}) => { if (body === null) { // Body is null dest.end(); } else { // Body is stream await pipeline(body, dest); } };