mirror of
https://github.com/oven-sh/setup-bun.git
synced 2025-04-08 05:56:03 +08:00
398 lines
9.6 KiB
JavaScript
398 lines
9.6 KiB
JavaScript
|
|
/**
|
|
* 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<void>}
|
|
*/
|
|
export const writeToStream = async (dest, {body}) => {
|
|
if (body === null) {
|
|
// Body is null
|
|
dest.end();
|
|
} else {
|
|
// Body is stream
|
|
await pipeline(body, dest);
|
|
}
|
|
};
|