New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@web-std/fetch

Package Overview
Dependencies
Maintainers
1
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@web-std/fetch - npm Package Compare versions

Comparing version
3.0.2
to
3.0.3
+1999
dist/index.cjs
'use strict';
exports = module.exports = fetch;
const http = require('http');
const https = require('https');
const zlib = require('zlib');
const dataUriToBuffer = require('data-uri-to-buffer');
const Stream = require('stream');
const util = require('util');
const blob = require('@web-std/blob');
const formData = require('@web-std/form-data');
const crypto = require('crypto');
const multipartParser = require('@web3-storage/multipart-parser');
const url = require('url');
class FetchBaseError extends Error {
/**
* @param {string} message
* @param {string} type
*/
constructor(message, type) {
super(message);
// Hide custom error implementation details from end-users
Error.captureStackTrace(this, this.constructor);
this.type = type;
}
get name() {
return this.constructor.name;
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
}
/**
* @typedef {{
* address?: string
* code: string
* dest?: string
* errno: number
* info?: object
* message: string
* path?: string
* port?: number
* syscall: string
* }} SystemError
*/
/**
* FetchError interface for operational errors
*/
class FetchError extends FetchBaseError {
/**
* @param {string} message - Error message for human
* @param {string} type - Error type for machine
* @param {SystemError} [systemError] - For Node.js system error
*/
constructor(message, type, systemError) {
super(message, type);
// When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code
if (systemError) {
// eslint-disable-next-line no-multi-assign
this.code = this.errno = systemError.code;
this.erroredSysCall = systemError.syscall;
}
}
}
/**
* Is.js
*
* Object type checks.
*/
const NAME = Symbol.toStringTag;
/**
* Check if `obj` is a URLSearchParams object
* ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143
*
* @param {any} object
* @return {obj is URLSearchParams}
*/
const isURLSearchParameters = (object) => {
return (
typeof object === "object" &&
typeof object.append === "function" &&
typeof object.delete === "function" &&
typeof object.get === "function" &&
typeof object.getAll === "function" &&
typeof object.has === "function" &&
typeof object.set === "function" &&
typeof object.sort === "function" &&
object[NAME] === "URLSearchParams"
);
};
/**
* Check if `object` is a W3C `Blob` object (which `File` inherits from)
*
* @param {*} object
* @return {object is Blob}
*/
const isBlob = (object) => {
return (
typeof object === "object" &&
typeof object.arrayBuffer === "function" &&
typeof object.type === "string" &&
typeof object.stream === "function" &&
typeof object.constructor === "function" &&
/^(Blob|File)$/.test(object[NAME])
);
};
/**
* Check if `obj` is a spec-compliant `FormData` object
*
* @param {*} object
* @return {object is FormData}
*/
function isFormData(object) {
return (
typeof object === "object" &&
typeof object.append === "function" &&
typeof object.set === "function" &&
typeof object.get === "function" &&
typeof object.getAll === "function" &&
typeof object.delete === "function" &&
typeof object.keys === "function" &&
typeof object.values === "function" &&
typeof object.entries === "function" &&
typeof object.constructor === "function" &&
object[NAME] === "FormData"
);
}
/**
* Detect form data input from form-data module
*
* @param {any} value
* @returns {value is Stream & {getBoundary():string, hasKnownLength():boolean, getLengthSync():number|null}}
*/
const isMultipartFormDataStream = (value) => {
return (
value instanceof Stream === true &&
typeof value.getBoundary === "function" &&
typeof value.hasKnownLength === "function" &&
typeof value.getLengthSync === "function"
);
};
/**
* Check if `obj` is an instance of AbortSignal.
*
* @param {any} object
* @return {obj is AbortSignal}
*/
const isAbortSignal = (object) => {
return (
typeof object === "object" &&
(object[NAME] === "AbortSignal" || object[NAME] === "EventTarget")
);
};
/**
* Check if `value` is a ReadableStream.
*
* @param {*} value
* @returns {value is ReadableStream}
*/
const isReadableStream = (value) => {
return (
typeof value === "object" &&
typeof value.getReader === "function" &&
typeof value.cancel === "function" &&
typeof value.tee === "function"
);
};
/**
*
* @param {any} value
* @returns {value is Iterable<unknown>}
*/
const isIterable = (value) => value && Symbol.iterator in value;
const carriage = '\r\n';
const dashes = '-'.repeat(2);
const carriageLength = Buffer.byteLength(carriage);
/**
* @param {string} boundary
*/
const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`;
/**
* @param {string} boundary
* @param {string} name
* @param {*} field
*
* @return {string}
*/
function getHeader(boundary, name, field) {
let header = '';
header += `${dashes}${boundary}${carriage}`;
header += `Content-Disposition: form-data; name="${name}"`;
if (isBlob(field)) {
const { name = 'blob', type } = /** @type {Blob & {name?:string}} */ (field);
header += `; filename="${name}"${carriage}`;
header += `Content-Type: ${type || 'application/octet-stream'}`;
}
return `${header}${carriage.repeat(2)}`;
}
/**
* @return {string}
*/
const getBoundary = () => crypto.randomBytes(8).toString('hex');
/**
* @param {FormData} form
* @param {string} boundary
*/
async function * formDataIterator(form, boundary) {
for (const [name, value] of form) {
yield getHeader(boundary, name, value);
if (isBlob(value)) {
// @ts-ignore - we know our streams implement aysnc iteration
yield * value.stream();
} else {
yield value;
}
yield carriage;
}
yield getFooter(boundary);
}
/**
* @param {FormData} form
* @param {string} boundary
*/
function getFormDataLength(form, boundary) {
let length = 0;
for (const [name, value] of form) {
length += Buffer.byteLength(getHeader(boundary, name, value));
if (isBlob(value)) {
length += value.size;
} else {
length += Buffer.byteLength(String(value));
}
length += carriageLength;
}
length += Buffer.byteLength(getFooter(boundary));
return length;
}
/**
* @param {Body & {headers?:Headers}} source
*/
const toFormData = async ({ body, headers }) => {
const contentType = headers?.get('Content-Type') || '';
const [type, boundary] = contentType.split(/\s*;\s*boundary=/);
if (type === 'multipart/form-data' && boundary != null && body != null) {
const form = new FormData();
const parts = multipartParser.iterateMultipart(body, boundary);
for await (const { name, data, filename, contentType } of parts) {
if (filename) {
form.append(name, new File([data], filename, { type: contentType }));
} else {
form.append(name, new TextDecoder().decode(data), filename);
}
}
return form
} else {
throw new TypeError('Could not parse content as FormData.')
}
};
const encoder = new util.TextEncoder();
const decoder = new util.TextDecoder();
/**
* @param {string} text
*/
const encode = text => encoder.encode(text);
/**
* @param {Uint8Array} bytes
*/
const decode = bytes => decoder.decode(bytes);
// @ts-check
const {readableHighWaterMark} = new Stream.Readable();
const INTERNALS$2 = Symbol('Body internals');
/**
* Body mixin
*
* Ref: https://fetch.spec.whatwg.org/#body
* @implements {globalThis.Body}
*/
class Body {
/**
* @param {BodyInit|Stream|null} body
* @param {{size?:number}} options
*/
constructor(body, {
size = 0
} = {}) {
const state = {
/** @type {null|ReadableStream<Uint8Array>} */
body: null,
/** @type {string|null} */
type: null,
/** @type {number|null} */
size: null,
/** @type {null|string} */
boundary: null,
disturbed: false,
/** @type {null|Error} */
error: null
};
/** @private */
this[INTERNALS$2] = state;
if (body === null) {
// Body is undefined or null
state.body = null;
state.size = 0;
} else if (isURLSearchParameters(body)) {
// Body is a URLSearchParams
const bytes = encode(body.toString());
state.body = fromBytes(bytes);
state.size = bytes.byteLength;
state.type = 'application/x-www-form-urlencoded;charset=UTF-8';
} else if (isBlob(body)) {
// Body is blob
state.size = body.size;
state.type = body.type || null;
state.body = body.stream();
} else if (body instanceof Uint8Array) {
// Body is Buffer
state.body = fromBytes(body);
state.size = body.byteLength;
} else if (util.types.isAnyArrayBuffer(body)) {
// Body is ArrayBuffer
const bytes = new Uint8Array(body);
state.body = fromBytes(bytes);
state.size = bytes.byteLength;
} else if (ArrayBuffer.isView(body)) {
// Body is ArrayBufferView
const bytes = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
state.body = fromBytes(bytes);
state.size = bytes.byteLength;
} else if (isReadableStream(body)) {
// Body is stream
state.body = body;
} else if (isFormData(body)) {
// Body is an instance of formdata-node
const boundary = `NodeFetchFormDataBoundary${getBoundary()}`;
state.type = `multipart/form-data; boundary=${boundary}`;
state.size = getFormDataLength(body, boundary);
state.body = fromAsyncIterable(formDataIterator(body, boundary));
} else if (isMultipartFormDataStream(body)) {
state.type = `multipart/form-data; boundary=${body.getBoundary()}`;
state.size = body.hasKnownLength() ? body.getLengthSync() : null;
state.body = fromStream(body);
} else if (body instanceof Stream) {
state.body = fromStream(body);
} else {
// None of the above
// coerce to string then buffer
const bytes = encode(String(body));
state.type = 'text/plain;charset=UTF-8';
state.size = bytes.byteLength;
state.body = fromBytes(bytes);
}
this.size = size;
// if (body instanceof Stream) {
// body.on('error', err => {
// const error = err instanceof FetchBaseError ?
// err :
// new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err);
// this[INTERNALS].error = error;
// });
// }
}
/** @type {Headers} */
/* c8 ignore next 3 */
get headers() {
throw new TypeError(`'get headers' called on an object that does not implements interface.`)
}
get body() {
return this[INTERNALS$2].body;
}
get bodyUsed() {
return this[INTERNALS$2].disturbed;
}
/**
* Decode response as ArrayBuffer
*
* @return {Promise<ArrayBuffer>}
*/
async arrayBuffer() {
const {buffer, byteOffset, byteLength} = await consumeBody(this);
return buffer.slice(byteOffset, byteOffset + byteLength);
}
/**
* Return raw response as Blob
*
* @return Promise
*/
async blob() {
const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS$2].body && this[INTERNALS$2].type) || '';
const buf = await consumeBody(this);
return new blob.Blob([buf], {
type: ct
});
}
/**
* Decode response as json
*
* @return Promise
*/
async json() {
return JSON.parse(await this.text());
}
/**
* Decode response as text
*
* @return Promise
*/
async text() {
const buffer = await consumeBody(this);
return decode(buffer);
}
/**
* @returns {Promise<FormData>}
*/
async formData() {
return toFormData(this)
}
}
// 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},
formData: {enumerable: true}
});
/**
* Consume and convert an entire Body to a Buffer.
*
* Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
*
* @param {Body & {url?:string}} data
* @return {Promise<Uint8Array>}
*/
async function consumeBody(data) {
const state = data[INTERNALS$2];
if (state.disturbed) {
throw new TypeError(`body used already for: ${data.url}`);
}
state.disturbed = true;
if (state.error) {
throw state.error;
}
const {body} = state;
// Body is null
if (body === null) {
return new Uint8Array(0);
}
// Body is stream
// get ready to actually consume the body
/** @type {[Uint8Array|null, Uint8Array[], number]} */
const [buffer, chunks, limit] = data.size > 0 ?
[new Uint8Array(data.size), [], data.size] :
[null, [], Infinity];
let offset = 0;
const source = streamIterator(body);
try {
for await (const chunk of source) {
const bytes = chunk instanceof Uint8Array ?
chunk :
Buffer.from(chunk);
if (offset + bytes.byteLength > limit) {
const error = new FetchError(`content size at ${data.url} over limit: ${limit}`, 'max-size');
source.throw(error);
throw error;
} else if (buffer) {
buffer.set(bytes, offset);
} else {
chunks.push(bytes);
}
offset += bytes.byteLength;
}
if (buffer) {
if (offset < buffer.byteLength) {
throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`, 'premature-close');
} else {
return buffer;
}
} else {
return writeBytes(new Uint8Array(offset), chunks);
}
} catch (error) {
if (error instanceof FetchBaseError) {
throw error;
// @ts-expect-error - we know it will have a name
} else if (error && error.name === 'AbortError') {
throw error;
} else {
const e = /** @type {import('./errors/fetch-error').SystemError} */(error);
// Other errors, such as incorrect content-encoding
throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${e.message}`, 'system', e);
}
}
}
/**
* Clone body given Res/Req instance
*
* @param {Body} instance Response or Request instance
* @return {ReadableStream<Uint8Array>}
*/
const clone = instance => {
const {body} = instance;
// Don't allow cloning a used body
if (instance.bodyUsed) {
throw new Error('cannot clone body after it is used');
}
// @ts-expect-error - could be null
const [left, right] = body.tee();
instance[INTERNALS$2].body = left;
return right;
};
/**
* 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 {Body} source Any options.body input
* @returns {string | null}
*/
const extractContentType = source => source[INTERNALS$2].type;
/**
* 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 {Body} source - Body object from the Body instance.
* @returns {number | null}
*/
const getTotalBytes = source => source[INTERNALS$2].size;
/**
* Write a Body to a Node.js WritableStream (e.g. http.Request) object.
*
* @param {Stream.Writable} dest - The stream to write to.
* @param {Body} source - Body object from the Body instance.
* @returns {void}
*/
const writeToStream = (dest, {body}) => {
if (body === null) {
// Body is null
dest.end();
} else {
Stream.Readable.from(streamIterator(body)).pipe(dest);
}
};
/**
* @template T
* @implements {AsyncGenerator<T, void, void>}
*/
class StreamIterableIterator {
/**
* @param {ReadableStream<T>} stream
*/
constructor(stream) {
this.stream = stream;
this.reader = null;
}
/**
* @returns {AsyncGenerator<T, void, void>}
*/
[Symbol.asyncIterator]() {
return this;
}
getReader() {
if (this.reader) {
return this.reader;
}
const reader = this.stream.getReader();
this.reader = reader;
return reader;
}
/**
* @returns {Promise<IteratorResult<T, void>>}
*/
next() {
return /** @type {Promise<IteratorResult<T, void>>} */ (this.getReader().read());
}
/**
* @returns {Promise<IteratorResult<T, void>>}
*/
async return() {
if (this.reader) {
await this.reader.cancel();
}
return {done: true, value: undefined};
}
/**
*
* @param {any} error
* @returns {Promise<IteratorResult<T, void>>}
*/
async throw(error) {
await this.getReader().cancel(error);
return {done: true, value: undefined};
}
}
/**
* @template T
* @param {ReadableStream<T>} stream
*/
const streamIterator = stream => new StreamIterableIterator(stream);
/**
* @param {Uint8Array} buffer
* @param {Uint8Array[]} chunks
*/
const writeBytes = (buffer, chunks) => {
let offset = 0;
for (const chunk of chunks) {
buffer.set(chunk, offset);
offset += chunk.byteLength;
}
return buffer;
};
/**
* @param {Uint8Array} bytes
* @returns {ReadableStream<Uint8Array>}
*/
// @ts-ignore
const fromBytes = bytes => new blob.ReadableStream({
start(controller) {
controller.enqueue(bytes);
controller.close();
}
});
/**
* @param {AsyncIterable<Uint8Array>} content
* @returns {ReadableStream<Uint8Array>}
*/
const fromAsyncIterable = content =>
// @ts-ignore
new blob.ReadableStream(new AsyncIterablePump(content));
/**
* @implements {UnderlyingSource<Uint8Array>}
*/
class AsyncIterablePump {
/**
* @param {AsyncIterable<Uint8Array>} source
*/
constructor(source) {
this.source = source[Symbol.asyncIterator]();
}
/**
* @param {ReadableStreamController<Uint8Array>} controller
*/
async pull(controller) {
try {
while (controller.desiredSize || 0 > 0) {
// eslint-disable-next-line no-await-in-loop
const next = await this.source.next();
if (next.done) {
controller.close();
break;
} else {
controller.enqueue(next.value);
}
}
} catch (error) {
controller.error(error);
}
}
/**
* @param {any} [reason]
*/
cancel(reason) {
if (reason) {
if (typeof this.source.throw === 'function') {
this.source.throw(reason);
} else if (typeof this.source.return === 'function') {
this.source.return();
}
} else if (typeof this.source.return === 'function') {
this.source.return();
}
}
}
/**
* @param {Stream & {readableHighWaterMark?:number}} source
* @returns {ReadableStream<Uint8Array>}
*/
const fromStream = source => {
const pump = new StreamPump(source);
const stream = new blob.ReadableStream(pump, pump);
return stream;
};
/**
* @implements {UnderlyingSource<Uint8Array>}
* @implements {QueuingStrategy<Uint8Array>}
*/
class StreamPump {
/**
* @param {Stream & {
* readableHighWaterMark?: number
* readable?:boolean,
* resume?: () => void,
* pause?: () => void
* destroy?: (error?:Error) => void
* }} stream
*/
constructor(stream) {
this.highWaterMark = stream.readableHighWaterMark || readableHighWaterMark;
this.accumalatedSize = 0;
this.stream = stream;
this.enqueue = this.enqueue.bind(this);
this.error = this.error.bind(this);
this.close = this.close.bind(this);
}
/**
* @param {Uint8Array} [chunk]
*/
size(chunk) {
return chunk?.byteLength || 0;
}
/**
* @param {ReadableStreamController<Uint8Array>} controller
*/
start(controller) {
this.controller = controller;
this.stream.on('data', this.enqueue);
this.stream.once('error', this.error);
this.stream.once('end', this.close);
this.stream.once('close', this.close);
}
pull() {
this.resume();
}
/**
* @param {any} [reason]
*/
cancel(reason) {
if (this.stream.destroy) {
this.stream.destroy(reason);
}
this.stream.off('data', this.enqueue);
this.stream.off('error', this.error);
this.stream.off('end', this.close);
this.stream.off('close', this.close);
}
/**
* @param {Uint8Array|string} chunk
*/
enqueue(chunk) {
if (this.controller) {
try {
const bytes = chunk instanceof Uint8Array ?
chunk :
Buffer.from(chunk);
const available = (this.controller.desiredSize || 0) - bytes.byteLength;
this.controller.enqueue(bytes);
if (available <= 0) {
this.pause();
}
} catch {
this.controller.error(new Error('Could not create Buffer, chunk must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object'));
this.cancel();
}
}
}
pause() {
if (this.stream.pause) {
this.stream.pause();
}
}
resume() {
if (this.stream.readable && this.stream.resume) {
this.stream.resume();
}
}
close() {
if (this.controller) {
this.controller.close();
delete this.controller;
}
}
/**
* @param {Error} error
*/
error(error) {
if (this.controller) {
this.controller.error(error);
delete this.controller;
}
}
}
/**
* Headers.js
*
* Headers class offers convenient helpers
*/
const validators = /** @type {{validateHeaderName?:(name:string) => any, validateHeaderValue?:(name:string, value:string) => any}} */
(http);
const validateHeaderName = typeof validators.validateHeaderName === 'function' ?
validators.validateHeaderName :
/**
* @param {string} name
*/
name => {
if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) {
const err = new TypeError(`Header name must be a valid HTTP token [${name}]`);
Object.defineProperty(err, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'});
throw err;
}
};
const validateHeaderValue = typeof validators.validateHeaderValue === 'function' ?
validators.validateHeaderValue :
/**
* @param {string} name
* @param {string} value
*/
(name, value) => {
if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) {
const err = new TypeError(`Invalid character in header content ["${name}"]`);
Object.defineProperty(err, 'code', {value: 'ERR_INVALID_CHAR'});
throw err;
}
};
/**
* @typedef {Headers | Record<string, string> | Iterable<readonly [string, string]> | Iterable<Iterable<string>>} HeadersInit
*/
/**
* This Fetch API interface allows you to perform various actions on HTTP request and response headers.
* These actions include retrieving, setting, adding to, and removing.
* A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs.
* You can add to this using methods like append() (see Examples.)
* In all methods of this interface, header names are matched by case-insensitive byte sequence.
*
* @implements {globalThis.Headers}
*/
class Headers extends URLSearchParams {
/**
* Headers class
*
* @constructor
* @param {HeadersInit} [init] - Response headers
*/
constructor(init) {
// Validate and normalize init object in [name, value(s)][]
/** @type {string[][]} */
let result = [];
if (init instanceof Headers) {
const raw = init.raw();
for (const [name, values] of Object.entries(raw)) {
result.push(...values.map(value => [name, value]));
}
} else if (init == null) ; else if (isIterable(init)) {
// Sequence<sequence<ByteString>>
// Note: per spec we have to first exhaust the lists then process them
result = [...init]
.map(pair => {
if (
typeof pair !== 'object' || util.types.isBoxedPrimitive(pair)
) {
throw new TypeError('Each header pair must be an iterable object');
}
return [...pair];
}).map(pair => {
if (pair.length !== 2) {
throw new TypeError('Each header pair must be a name/value tuple');
}
return [...pair];
});
} else if (typeof init === "object" && init !== null) {
// Record<ByteString, ByteString>
result.push(...Object.entries(init));
} else {
throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)');
}
// Validate and lowercase
result =
result.length > 0 ?
result.map(([name, value]) => {
validateHeaderName(name);
validateHeaderValue(name, String(value));
return [String(name).toLowerCase(), String(value)];
}) :
[];
super(result);
// Returning a Proxy that will lowercase key names, validate parameters and sort keys
// eslint-disable-next-line no-constructor-return
return new Proxy(this, {
get(target, p, receiver) {
switch (p) {
case 'append':
case 'set':
/**
* @param {string} name
* @param {string} value
*/
return (name, value) => {
validateHeaderName(name);
validateHeaderValue(name, String(value));
return URLSearchParams.prototype[p].call(
receiver,
String(name).toLowerCase(),
String(value)
);
};
case 'delete':
case 'has':
case 'getAll':
/**
* @param {string} name
*/
return name => {
validateHeaderName(name);
// @ts-ignore
return URLSearchParams.prototype[p].call(
receiver,
String(name).toLowerCase()
);
};
case 'keys':
return () => {
target.sort();
return new Set(URLSearchParams.prototype.keys.call(target)).keys();
};
default:
return Reflect.get(target, p, receiver);
}
}
/* c8 ignore next */
});
}
get [Symbol.toStringTag]() {
return this.constructor.name;
}
toString() {
return Object.prototype.toString.call(this);
}
/**
*
* @param {string} name
*/
get(name) {
const values = this.getAll(name);
if (values.length === 0) {
return null;
}
let value = values.join(', ');
if (/^content-encoding$/i.test(name)) {
value = value.toLowerCase();
}
return value;
}
/**
* @param {(value: string, key: string, parent: this) => void} callback
* @param {any} thisArg
* @returns {void}
*/
forEach(callback, thisArg = undefined) {
for (const name of this.keys()) {
Reflect.apply(callback, thisArg, [this.get(name), name, this]);
}
}
/**
* @returns {IterableIterator<string>}
*/
* values() {
for (const name of this.keys()) {
yield /** @type {string} */(this.get(name));
}
}
/**
* @returns {IterableIterator<[string, string]>}
*/
* entries() {
for (const name of this.keys()) {
yield [name, /** @type {string} */(this.get(name))];
}
}
[Symbol.iterator]() {
return this.entries();
}
/**
* Node-fetch non-spec method
* returning all headers and their values as array
* @returns {Record<string, string[]>}
*/
raw() {
return [...this.keys()].reduce((result, key) => {
result[key] = this.getAll(key);
return result;
}, /** @type {Record<string, string[]>} */({}));
}
/**
* For better console.log(headers) and also to convert Headers into Node.js Request compatible format
*/
[Symbol.for('nodejs.util.inspect.custom')]() {
return [...this.keys()].reduce((result, key) => {
const values = this.getAll(key);
// Http.request() only supports string as Host header.
// This hack makes specifying custom Host header possible.
if (key === 'host') {
result[key] = values[0];
} else {
result[key] = values.length > 1 ? values : values[0];
}
return result;
}, /** @type {Record<string, string|string[]>} */({}));
}
}
/**
* Re-shaping object for Web IDL tests
* Only need to do it for overridden methods
*/
Object.defineProperties(
Headers.prototype,
['get', 'entries', 'forEach', 'values'].reduce((result, property) => {
result[property] = {enumerable: true};
return result;
}, /** @type {Record<string, {enumerable:true}>} */ ({}))
);
/**
* Create a Headers object from an http.IncomingMessage.rawHeaders, ignoring those that do
* not conform to HTTP grammar productions.
* @param {import('http').IncomingMessage['rawHeaders']} headers
*/
function fromRawHeaders(headers = []) {
return new Headers(
headers
// Split into pairs
.reduce((result, value, index, array) => {
if (index % 2 === 0) {
result.push(array.slice(index, index + 2));
}
return result;
}, /** @type {string[][]} */([]))
.filter(([name, value]) => {
try {
validateHeaderName(name);
validateHeaderValue(name, String(value));
return true;
} catch {
return false;
}
})
);
}
const redirectStatus = new Set([301, 302, 303, 307, 308]);
/**
* Redirect code matching
*
* @param {number} code - Status code
* @return {boolean}
*/
const isRedirect = code => {
return redirectStatus.has(code);
};
/**
* Response.js
*
* Response class provides content decoding
*/
const INTERNALS$1 = Symbol('Response internals');
/**
* Response class
*
* @typedef {Object} Ext
* @property {number} [size]
* @property {string} [url]
* @property {number} [counter]
* @property {number} [highWaterMark]
*
* @implements {globalThis.Response}
*/
class Response extends Body {
/**
* @param {BodyInit|import('stream').Stream|null} [body] - Readable stream
* @param {ResponseInit & Ext} [options] - Response options
*/
constructor(body = null, options = {}) {
super(body, options);
const status = options.status || 200;
const headers = new Headers(options.headers);
if (body !== null && !headers.has('Content-Type')) {
const contentType = extractContentType(this);
if (contentType) {
headers.append('Content-Type', contentType);
}
}
/**
* @private
*/
this[INTERNALS$1] = {
url: options.url,
status,
statusText: options.statusText || '',
headers,
counter: options.counter || 0,
highWaterMark: options.highWaterMark
};
}
/**
* @type {ResponseType}
*/
get type() {
return "default"
}
get url() {
return this[INTERNALS$1].url || '';
}
get status() {
return this[INTERNALS$1].status;
}
/**
* Convenience property representing if the request ended normally
*/
get ok() {
return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300;
}
get redirected() {
return this[INTERNALS$1].counter > 0;
}
get statusText() {
return this[INTERNALS$1].statusText;
}
/**
* @type {Headers}
*/
get headers() {
return this[INTERNALS$1].headers;
}
get highWaterMark() {
return this[INTERNALS$1].highWaterMark;
}
/**
* Clone this response
*
* @returns {Response}
*/
clone() {
return new Response(clone(this), {
url: this.url,
status: this.status,
statusText: this.statusText,
headers: this.headers,
size: this.size
});
}
/**
* @param {string} url The URL that the new response is to originate from.
* @param {number} status An optional status code for the response (e.g., 302.)
* @returns {Response} A Response object.
*/
static redirect(url, status = 302) {
if (!isRedirect(status)) {
throw new RangeError('Failed to execute "redirect" on "response": Invalid status code');
}
return new Response(null, {
headers: {
location: new URL(url).toString()
},
status
});
}
get [Symbol.toStringTag]() {
return 'Response';
}
}
Object.defineProperties(Response.prototype, {
url: {enumerable: true},
status: {enumerable: true},
ok: {enumerable: true},
redirected: {enumerable: true},
statusText: {enumerable: true},
headers: {enumerable: true},
clone: {enumerable: true}
});
/**
* @param {URL} parsedURL
* @returns {string}
*/
const getSearch = parsedURL => {
if (parsedURL.search) {
return parsedURL.search;
}
const lastOffset = parsedURL.href.length - 1;
const hash = parsedURL.hash || (parsedURL.href[lastOffset] === '#' ? '#' : '');
return parsedURL.href[lastOffset - hash.length] === '?' ? '?' : '';
};
const INTERNALS = Symbol('Request internals');
/**
* Check if `obj` is an instance of Request.
*
* @param {any} object
* @return {object is Request}
*/
const isRequest = object => {
return (
typeof object === 'object' &&
typeof object[INTERNALS] === 'object'
);
};
/**
* Request class
* @implements {globalThis.Request}
*
* @typedef {Object} RequestState
* @property {string} method
* @property {RequestRedirect} redirect
* @property {globalThis.Headers} headers
* @property {URL} parsedURL
* @property {AbortSignal|null} signal
*
* @typedef {Object} RequestExtraOptions
* @property {number} [follow]
* @property {boolean} [compress]
* @property {number} [size]
* @property {number} [counter]
* @property {Agent} [agent]
* @property {number} [highWaterMark]
* @property {boolean} [insecureHTTPParser]
*
* @typedef {((url:URL) => import('http').Agent) | import('http').Agent} Agent
*
* @typedef {Object} RequestOptions
* @property {string} [method]
* @property {ReadableStream<Uint8Array>|null} [body]
* @property {globalThis.Headers} [headers]
* @property {RequestRedirect} [redirect]
*
*/
class Request extends Body {
/**
* @param {string|Request|URL} info Url or Request instance
* @param {RequestInit & RequestExtraOptions} init Custom options
*/
constructor(info, init = {}) {
let parsedURL;
/** @type {RequestOptions & RequestExtraOptions} */
let settings;
// Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245)
if (isRequest(info)) {
parsedURL = new URL(info.url);
settings = (info);
} else {
parsedURL = new URL(info);
settings = {};
}
let method = init.method || settings.method || 'GET';
method = method.toUpperCase();
const inputBody = init.body != null
? init.body
: (isRequest(info) && info.body !== null)
? clone(info)
: null;
// eslint-disable-next-line no-eq-null, eqeqeq
if (inputBody != null && (method === 'GET' || method === 'HEAD')) {
throw new TypeError('Request with GET/HEAD method cannot have body');
}
super(inputBody, {
size: init.size || settings.size || 0
});
const input = settings;
const headers = /** @type {globalThis.Headers} */
(new Headers(init.headers || input.headers || {}));
if (inputBody !== null && !headers.has('Content-Type')) {
const contentType = extractContentType(this);
if (contentType) {
headers.append('Content-Type', contentType);
}
}
let signal = 'signal' in init
? init.signal
: isRequest(input)
? input.signal
: null;
// eslint-disable-next-line no-eq-null, eqeqeq
if (signal != null && !isAbortSignal(signal)) {
throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget');
}
/** @type {RequestState} */
this[INTERNALS] = {
method,
redirect: init.redirect || input.redirect || 'follow',
headers,
parsedURL,
signal: signal || null
};
/** @type {boolean} */
this.keepalive;
// Node-fetch-only options
/** @type {number} */
this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow;
/** @type {boolean} */
this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress;
/** @type {number} */
this.counter = init.counter || input.counter || 0;
/** @type {Agent|undefined} */
this.agent = init.agent || input.agent;
/** @type {number} */
this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384;
/** @type {boolean} */
this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false;
}
/**
* @type {RequestCache}
*/
get cache() {
return "default"
}
/**
* @type {RequestCredentials}
*/
get credentials() {
return "same-origin"
}
/**
* @type {RequestDestination}
*/
get destination() {
return ""
}
get integrity() {
return ""
}
/** @type {RequestMode} */
get mode() {
return "cors"
}
/** @type {string} */
get referrer() {
return ""
}
/** @type {ReferrerPolicy} */
get referrerPolicy() {
return ""
}
get method() {
return this[INTERNALS].method;
}
/**
* @type {string}
*/
get url() {
return url.format(this[INTERNALS].parsedURL);
}
/**
* @type {globalThis.Headers}
*/
get headers() {
return this[INTERNALS].headers;
}
get redirect() {
return this[INTERNALS].redirect;
}
/**
* @returns {AbortSignal}
*/
get signal() {
// @ts-ignore
return this[INTERNALS].signal;
}
/**
* Clone this request
*
* @return {globalThis.Request}
*/
clone() {
return new Request(this);
}
get [Symbol.toStringTag]() {
return 'Request';
}
}
Object.defineProperties(Request.prototype, {
method: {enumerable: true},
url: {enumerable: true},
headers: {enumerable: true},
redirect: {enumerable: true},
clone: {enumerable: true},
signal: {enumerable: true}
});
/**
* Convert a Request to Node.js http request options.
* The options object to be passed to http.request
*
* @param {Request & Record<INTERNALS, RequestState>} request - A Request instance
*/
const getNodeRequestOptions = request => {
const {parsedURL} = request[INTERNALS];
const headers = new Headers(request[INTERNALS].headers);
// Fetch step 1.3
if (!headers.has('Accept')) {
headers.set('Accept', '*/*');
}
// HTTP-network-or-cache fetch steps 2.4-2.7
let contentLengthValue = null;
if (request.body === null && /^(post|put)$/i.test(request.method)) {
contentLengthValue = '0';
}
if (request.body !== null) {
const totalBytes = getTotalBytes(request);
// Set Content-Length if totalBytes is a number (that is not NaN)
if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
contentLengthValue = String(totalBytes);
}
}
if (contentLengthValue) {
headers.set('Content-Length', contentLengthValue);
}
// HTTP-network-or-cache fetch step 2.11
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'node-fetch');
}
// HTTP-network-or-cache fetch step 2.15
if (request.compress && !headers.has('Accept-Encoding')) {
headers.set('Accept-Encoding', 'gzip,deflate,br');
}
let {agent} = request;
if (typeof agent === 'function') {
agent = agent(parsedURL);
}
if (!headers.has('Connection') && !agent) {
headers.set('Connection', 'close');
}
// HTTP-network fetch step 4.2
// chunked encoding is handled by Node.js
const search = getSearch(parsedURL);
// Manually spread the URL object instead of spread syntax
const requestOptions = {
path: parsedURL.pathname + search,
pathname: parsedURL.pathname,
hostname: parsedURL.hostname,
protocol: parsedURL.protocol,
port: parsedURL.port,
hash: parsedURL.hash,
search: parsedURL.search,
// @ts-ignore - it does not has a query
query: parsedURL.query,
href: parsedURL.href,
method: request.method,
// @ts-ignore - not sure what this supposed to do
headers: headers[Symbol.for('nodejs.util.inspect.custom')](),
insecureHTTPParser: request.insecureHTTPParser,
agent
};
return requestOptions;
};
/**
* AbortError interface for cancelled requests
*/
class AbortError extends FetchBaseError {
/**
* @param {string} message
* @param {string} [type]
*/
constructor(message, type = 'aborted') {
super(message, type);
}
}
/**
* Index.js
*
* a request API compatible with window.fetch
*
* All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
*/
const supportedSchemas = new Set(['data:', 'http:', 'https:']);
/**
* Fetch function
*
* @param {string | URL | import('./request').default} url - Absolute url or Request instance
* @param {RequestInit} [options_] - Fetch options
* @return {Promise<import('./response').default>}
*/
async function fetch(url, options_ = {}) {
return new Promise((resolve, reject) => {
// Build request object
const request = new Request(url, options_);
const options = getNodeRequestOptions(request);
if (!supportedSchemas.has(options.protocol)) {
throw new TypeError(`node-fetch cannot load ${url}. URL scheme "${options.protocol.replace(/:$/, '')}" is not supported.`);
}
if (options.protocol === 'data:') {
const data = dataUriToBuffer(request.url.toString());
const response = new Response(data, {headers: {'Content-Type': data.typeFull}});
resolve(response);
return;
}
// Wrap http.request into fetch
const send = (options.protocol === 'https:' ? https : http).request;
const {signal} = request;
/** @type {Response|null} */
let response = null;
/** @type {import('http').IncomingMessage|null} */
let response_ = null;
const abort = () => {
const error = new AbortError('The operation was aborted.');
reject(error);
if (request.body) {
request.body.cancel(error);
}
if (!response_) {
return;
}
response_.emit('error', error);
};
if (signal && signal.aborted) {
abort();
return;
}
const abortAndFinalize = () => {
abort();
finalize();
};
// Send request
const request_ = send(options);
if (signal) {
signal.addEventListener('abort', abortAndFinalize);
}
const finalize = () => {
request_.abort();
if (signal) {
signal.removeEventListener('abort', abortAndFinalize);
}
};
request_.on('error', err => {
// @ts-expect-error - err may not be SystemError
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
finalize();
});
fixResponseChunkedTransferBadEnding(request_, err => {
if (signal && signal.aborted) {
return
}
response_?.emit("error", err);
});
/* c8 ignore next 18 */
if (parseInt(process.version.substring(1)) < 14) {
// Before Node.js 14, pipeline() does not fully support async iterators and does not always
// properly handle when the socket close/end events are out of order.
request_.on('socket', s => {
s.prependListener('close', hadError => {
// if a data listener is still present we didn't end cleanly
const hasDataListener = s.listenerCount('data') > 0;
// if end happened before close but the socket didn't emit an error, do it now
if (response && hasDataListener && !hadError && !(signal && signal.aborted)) {
const err = Object.assign(new Error('Premature close'), {
code: 'ERR_STREAM_PREMATURE_CLOSE'
});
response_?.emit('error', err);
}
});
});
}
request_.on('response', incoming => {
response_ = incoming;
request_.setTimeout(0);
const headers = fromRawHeaders(response_.rawHeaders);
// HTTP fetch step 5
if (isRedirect(Number(response_.statusCode))) {
// HTTP fetch step 5.2
const location = headers.get('Location');
// HTTP fetch step 5.3
const locationURL = location === null ? null : new URL(location, request.url);
// HTTP fetch step 5.5
switch (request.redirect) {
case 'error':
reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect'));
finalize();
return;
case 'manual':
// Node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL.
if (locationURL !== null) {
headers.set('Location', locationURL.toString());
}
break;
case 'follow': {
// HTTP-redirect fetch step 2
if (locationURL === null) {
break;
}
// HTTP-redirect fetch step 5
if (request.counter >= request.follow) {
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
finalize();
return;
}
// HTTP-redirect fetch step 6 (counter increment)
// Create a new Request object.
const requestOptions = {
headers: new Headers(request.headers),
follow: request.follow,
counter: request.counter + 1,
agent: request.agent,
compress: request.compress,
method: request.method,
// Note: We can not use `request.body` because send would have
// consumed it already.
body: options_.body,
signal: request.signal,
size: request.size
};
// HTTP-redirect fetch step 9
const isStreamBody =
requestOptions.body instanceof blob.ReadableStream ||
requestOptions.body instanceof Stream.Readable;
if (response_.statusCode !== 303 && isStreamBody) {
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
finalize();
return;
}
// HTTP-redirect fetch step 11
if (response_.statusCode === 303 || ((response_.statusCode === 301 || response_.statusCode === 302) && request.method === 'POST')) {
requestOptions.method = 'GET';
requestOptions.body = undefined;
requestOptions.headers.delete('content-length');
}
// HTTP-redirect fetch step 15
fetch(new Request(locationURL.href, requestOptions)).then(resolve, reject);
finalize();
return;
}
default:
return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`));
}
}
// Prepare response
if (signal) {
response_.once('end', () => {
signal.removeEventListener('abort', abortAndFinalize);
});
}
let body = Stream.pipeline(response_, new Stream.PassThrough(), reject);
// see https://github.com/nodejs/node/pull/29376
/* c8 ignore next 3 */
if (process.version < 'v12.10') {
response_.on('aborted', abortAndFinalize);
}
const responseOptions = {
url: request.url,
status: response_.statusCode,
statusText: response_.statusMessage,
headers,
size: request.size,
counter: request.counter,
highWaterMark: request.highWaterMark
};
// HTTP-network fetch step 12.1.1.3
const codings = headers.get('Content-Encoding');
// HTTP-network fetch step 12.1.1.4: handle content codings
// in following scenarios we ignore compression support
// 1. compression support is disabled
// 2. HEAD request
// 3. no Content-Encoding header
// 4. no content response (204)
// 5. content not modified response (304)
if (!request.compress || request.method === 'HEAD' || codings === null || response_.statusCode === 204 || response_.statusCode === 304) {
response = new Response(body, responseOptions);
resolve(response);
return;
}
// For Node v6+
// Be less strict when decoding compressed responses, since sometimes
// servers send slightly invalid responses that are still accepted
// by common browsers.
// Always using Z_SYNC_FLUSH is what cURL does.
const zlibOptions = {
flush: zlib.Z_SYNC_FLUSH,
finishFlush: zlib.Z_SYNC_FLUSH
};
// For gzip
if (codings === 'gzip' || codings === 'x-gzip') {
body = Stream.pipeline(body, zlib.createGunzip(zlibOptions), reject);
response = new Response(fromAsyncIterable(body), responseOptions);
resolve(response);
return;
}
// For deflate
if (codings === 'deflate' || codings === 'x-deflate') {
// Handle the infamous raw deflate response from old servers
// a hack for old IIS and Apache servers
const raw = Stream.pipeline(response_, new Stream.PassThrough(), reject);
raw.once('data', chunk => {
// See http://stackoverflow.com/questions/37519828
if ((chunk[0] & 0x0F) === 0x08) {
body = Stream.pipeline(body, zlib.createInflate(), reject);
} else {
body = Stream.pipeline(body, zlib.createInflateRaw(), reject);
}
response = new Response(fromAsyncIterable(body), responseOptions);
resolve(response);
});
return;
}
// For br
if (codings === 'br') {
body = Stream.pipeline(body, zlib.createBrotliDecompress(), reject);
response = new Response(fromAsyncIterable(body), responseOptions);
resolve(response);
return;
}
// Otherwise, use response as-is
response = new Response(fromAsyncIterable(body), responseOptions);
resolve(response);
});
writeToStream(request_, request);
});
}
/**
*
* @param {import('http').ClientRequest} request
* @param {(error:Error) => void} errorCallback
*/
function fixResponseChunkedTransferBadEnding(request, errorCallback) {
/** @type {import('net').Socket} */
let socket;
request.on('socket', s => {
socket = s;
});
request.on('response', response => {
const {headers} = response;
if (headers['transfer-encoding'] === 'chunked' && !headers['content-length']) {
socket.prependListener('close', hadError => {
// if a data listener is still present we didn't end cleanly
const hasDataListener = socket.listenerCount('data') > 0;
if (hasDataListener && !hadError) {
const err = Object.assign(new Error('Premature close'), {
code: 'ERR_STREAM_PREMATURE_CLOSE'
});
errorCallback(err);
}
});
}
});
}
Object.defineProperty(exports, 'Blob', {
enumerable: true,
get: function () { return blob.Blob; }
});
Object.defineProperty(exports, 'ReadableStream', {
enumerable: true,
get: function () { return blob.ReadableStream; }
});
Object.defineProperty(exports, 'FormData', {
enumerable: true,
get: function () { return formData.FormData; }
});
exports.AbortError = AbortError;
exports.FetchError = FetchError;
exports.Headers = Headers;
exports.Request = Request;
exports.Response = Response;
exports["default"] = fetch;
exports.isRedirect = isRedirect;
//# sourceMappingURL=index.cjs.map

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

+2
-2
{
"name": "@web-std/fetch",
"version": "3.0.2",
"version": "3.0.3",
"description": "Web compatible Fetch API implementation for node.js",

@@ -36,3 +36,3 @@ "main": "./dist/index.cjs",

"src",
"dist/src",
"dist",
"License.md",

@@ -39,0 +39,0 @@ "Readme.md"