'use strict'; exports = module.exports = fetch; const http = require('http'); const https = require('https'); const zlib = require('zlib'); const Stream = require('stream'); const dataUriToBuffer = require('data-uri-to-buffer'); const util = require('util'); const Blob = require('fetch-blob'); const crypto = require('crypto'); const url = require('url'); class FetchBaseError extends Error { 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 {*} obj * @return {boolean} */ 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 {*} obj * @return {boolean} */ 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 {boolean} */ 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' ); } /** * Check if `obj` is an instance of AbortSignal. * * @param {*} obj * @return {boolean} */ const isAbortSignal = object => { return ( typeof object === 'object' && object[NAME] === 'AbortSignal' ); }; 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)) { header += `; filename="${field.name}"${carriage}`; header += `Content-Type: ${field.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)) { 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; } 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 */ 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)) ; else if (Buffer.isBuffer(body)) ; else if (util.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) ; else if (isFormData(body)) { // Body is an instance of formdata-node boundary = `NodeFetchFormDataBoundary${getBoundary()}`; body = Stream.Readable.from(formDataIterator(body, boundary)); } else { // None of the above // coerce to string then buffer body = Buffer.from(String(body)); } this[INTERNALS] = { body, boundary, disturbed: false, error: null }; 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; }); } } get body() { return this[INTERNALS].body; } 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); } /** * 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.buffer(); return new Blob([buf], { type: ct }); } /** * Decode response as json * * @return Promise */ async json() { const buffer = await consumeBody(this); return JSON.parse(buffer.toString()); } /** * Decode response as text * * @return Promise */ async text() { const buffer = await consumeBody(this); return buffer.toString(); } /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer() { return consumeBody(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} }); /** * 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; } let {body} = data; // Body is null if (body === null) { return Buffer.alloc(0); } // Body is blob if (isBlob(body)) { body = body.stream(); } // Body is buffer if (Buffer.isBuffer(body)) { return body; } /* 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 err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'); body.destroy(err); throw err; } accumBytes += chunk.length; accum.push(chunk); } } catch (error) { if (error instanceof FetchBaseError) { throw error; } else { // Other errors, such as incorrect content-encoding throw new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', 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 */ const clone = (instance, highWaterMark) => { let p1; let p2; let {body} = instance; // 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 Stream.PassThrough({highWaterMark}); p2 = new Stream.PassThrough({highWaterMark}); body.pipe(p1); body.pipe(p2); // Set instance body to teed body and return the other teed body instance[INTERNALS].body = p1; body = p2; } return body; }; /** * 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} */ 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) || util.types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) { return null; } // Detect form data input from form-data module if (body && typeof body.getBoundary === 'function') { return `multipart/form-data;boundary=${body.getBoundary()}`; } if (isFormData(body)) { return `multipart/form-data; boundary=${request[INTERNALS].boundary}`; } // 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} */ const getTotalBytes = request => { const {body} = request; // 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 a spec-compliant form-data if (isFormData(body)) { return getFormDataLength(request[INTERNALS].boundary); } // 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 {void} */ const writeToStream = (dest, {body}) => { if (body === null) { // Body is null dest.end(); } else if (isBlob(body)) { // Body is Blob body.stream().pipe(dest); } else if (Buffer.isBuffer(body)) { // Body is buffer dest.write(body); dest.end(); } else { // Body is stream body.pipe(dest); } }; /** * Headers.js * * Headers class offers convenient helpers */ const validateHeaderName = typeof http.validateHeaderName === 'function' ? http.validateHeaderName : 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 http.validateHeaderValue === 'function' ? http.validateHeaderValue : (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 | Iterable | Iterable>} 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. * */ 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 (typeof init === 'object' && !util.types.isBoxedPrimitive(init)) { const method = init[Symbol.iterator]; // eslint-disable-next-line no-eq-null, eqeqeq if (method == null) { // Record result.push(...Object.entries(init)); } else { if (typeof method !== 'function') { throw new TypeError('Header pairs must be iterable'); } // Sequence> // 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 { throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence> or record)'); } // Validate and lowercase result = result.length > 0 ? result.map(([name, value]) => { validateHeaderName(name); validateHeaderValue(name, String(value)); return [String(name).toLowerCase(), String(value)]; }) : undefined; 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': 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': return name => { validateHeaderName(name); 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); } 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; } forEach(callback) { for (const name of this.keys()) { callback(this.get(name), name); } } * values() { for (const name of this.keys()) { yield this.get(name); } } /** * @type {() => IterableIterator<[string, string]>} */ * entries() { for (const name of this.keys()) { yield [name, this.get(name)]; } } [Symbol.iterator]() { return this.entries(); } /** * Node-fetch non-spec method * returning all headers and their values as array * @returns {Record} */ raw() { return [...this.keys()].reduce((result, key) => { result[key] = this.getAll(key); return result; }, {}); } /** * 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; }, {}); } } /** * 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; }, {}) ); /** * 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; }, []) .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 * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ class Response extends Body { 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(body); if (contentType) { headers.append('Content-Type', contentType); } } this[INTERNALS$1] = { url: options.url, status, statusText: options.statusText || '', headers, counter: options.counter, highWaterMark: options.highWaterMark }; } 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; } get headers() { return this[INTERNALS$1].headers; } get highWaterMark() { return this[INTERNALS$1].highWaterMark; } /** * Clone this response * * @return Response */ clone() { return new Response(clone(this, this.highWaterMark), { url: this.url, status: this.status, statusText: this.statusText, headers: this.headers, ok: this.ok, redirected: this.redirected, 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} }); 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$2 = Symbol('Request internals'); /** * Check if `obj` is an instance of Request. * * @param {*} obj * @return {boolean} */ const isRequest = object => { return ( typeof object === 'object' && typeof object[INTERNALS$2] === 'object' ); }; /** * Request class * * @param Mixed input Url or Request instance * @param Object init Custom options * @return Void */ class Request extends Body { constructor(input, init = {}) { let parsedURL; // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245) if (isRequest(input)) { parsedURL = new URL(input.url); } else { parsedURL = new URL(input); input = {}; } let method = init.method || input.method || 'GET'; method = method.toUpperCase(); // eslint-disable-next-line no-eq-null, eqeqeq if (((init.body != null || isRequest(input)) && input.body !== null) && (method === 'GET' || method === 'HEAD')) { throw new TypeError('Request with GET/HEAD method cannot have body'); } const inputBody = init.body ? init.body : (isRequest(input) && input.body !== null ? clone(input) : null); super(inputBody, { size: init.size || input.size || 0 }); const headers = new Headers(init.headers || input.headers || {}); if (inputBody !== null && !headers.has('Content-Type')) { const contentType = extractContentType(inputBody, this); if (contentType) { headers.append('Content-Type', contentType); } } let signal = isRequest(input) ? input.signal : null; if ('signal' in init) { signal = init.signal; } if (signal !== null && !isAbortSignal(signal)) { throw new TypeError('Expected signal to be an instanceof AbortSignal'); } this[INTERNALS$2] = { method, redirect: init.redirect || input.redirect || 'follow', headers, parsedURL, signal }; // Node-fetch-only options this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow; this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; } get method() { return this[INTERNALS$2].method; } get url() { return url.format(this[INTERNALS$2].parsedURL); } get headers() { return this[INTERNALS$2].headers; } get redirect() { return this[INTERNALS$2].redirect; } get signal() { return this[INTERNALS$2].signal; } /** * Clone this request * * @return 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. * * @param Request A Request instance * @return Object The options object to be passed to http.request */ const getNodeRequestOptions = request => { const {parsedURL} = request[INTERNALS$2]; const headers = new Headers(request[INTERNALS$2].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, query: parsedURL.query, href: parsedURL.href, method: request.method, headers: headers[Symbol.for('nodejs.util.inspect.custom')](), insecureHTTPParser: request.insecureHTTPParser, agent }; return requestOptions; }; /** * AbortError interface for cancelled requests */ class AbortError extends FetchBaseError { 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 {*} [options_] - Fetch options * @return {Promise} */ 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); 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; let response = null; const abort = () => { const error = new AbortError('The operation was aborted.'); reject(error); if (request.body && request.body instanceof Stream.Readable) { request.body.destroy(error); } if (!response || !response.body) { return; } response.body.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 => { reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); finalize(); }); request_.on('response', response_ => { request_.setTimeout(0); const headers = fromRawHeaders(response_.rawHeaders); // HTTP fetch step 5 if (isRedirect(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) { // Handle corrupted header try { headers.set('Location', locationURL); /* c8 ignore next 3 */ } catch (error) { reject(error); } } 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, body: request.body, signal: request.signal }; // HTTP-redirect fetch step 9 if (response_.statusCode !== 303 && request.body && options_.body instanceof Stream.Readable) { 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 resolve(fetch(new Request(locationURL, requestOptions))); finalize(); return; } // Do nothing } } // Prepare response response_.once('end', () => { if (signal) { signal.removeEventListener('abort', abortAndFinalize); } }); let body = Stream.pipeline(response_, new Stream.PassThrough(), error => { reject(error); }); // see https://github.com/nodejs/node/pull/29376 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), error => { reject(error); }); response = new Response(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(), error => { reject(error); }); raw.once('data', chunk => { // See http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = Stream.pipeline(body, zlib.createInflate(), error => { reject(error); }); } else { body = Stream.pipeline(body, zlib.createInflateRaw(), error => { reject(error); }); } response = new Response(body, responseOptions); resolve(response); }); return; } // For br if (codings === 'br') { body = Stream.pipeline(body, zlib.createBrotliDecompress(), error => { reject(error); }); response = new Response(body, responseOptions); resolve(response); return; } // Otherwise, use response as-is response = new Response(body, responseOptions); resolve(response); }); writeToStream(request_, request); }); } 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