'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 url = require('url'); /** * Fetch-error.js * * FetchError interface for operational errors */ /** * Create FetchError instance * * @param String message Error message for human * @param String type Error type for machine * @param Object systemError For Node.js system error * @return FetchError */ class FetchError extends Error { constructor(message, type, systemError) { super(message); this.message = message; this.type = type; this.name = 'FetchError'; this[Symbol.toStringTag] = 'FetchError'; // 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; } // Hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); } } /** * 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 isURLSearchParams = 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 `obj` 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 an instance of AbortSignal. * * @param {*} obj * @return {boolean} */ const isAbortSignal = object => { return ( typeof object === 'object' && object[NAME] === 'AbortSignal' ); }; /** * Check if `obj` is an instance of AbortError. * * @param {*} obj * @return {boolean} */ const isAbortError = object => { return object[NAME] === 'AbortError'; }; 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, timeout = 0 } = {}) { if (body === null) { // Body is undefined or null body = null; } else if (isURLSearchParams(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 { // None of the above // coerce to string then buffer body = Buffer.from(String(body)); } this[INTERNALS] = { body, disturbed: false, error: null }; this.size = size; this.timeout = timeout; if (body instanceof Stream) { body.on('error', err => { const error = isAbortError(err) ? 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 consumeBody(this); return new Blob([], { type: ct.toLowerCase(), buffer: buf }); } /** * 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 */ const consumeBody = data => { if (data[INTERNALS].disturbed) { return Body.Promise.reject(new TypeError(`body used already for: ${data.url}`)); } data[INTERNALS].disturbed = true; if (data[INTERNALS].error) { return Body.Promise.reject(data[INTERNALS].error); } let {body} = data; // Body is null if (body === null) { return Body.Promise.resolve(Buffer.alloc(0)); } // Body is blob if (isBlob(body)) { body = body.stream(); } // Body is buffer if (Buffer.isBuffer(body)) { return Body.Promise.resolve(body); } // istanbul ignore if: should never happen if (!(body instanceof Stream)) { return Body.Promise.resolve(Buffer.alloc(0)); } // Body is stream // get ready to actually consume the body const accum = []; let accumBytes = 0; let abort = false; return new Body.Promise((resolve, reject) => { let resTimeout; // Allow timeout on slow response body if (data.timeout) { resTimeout = setTimeout(() => { abort = true; const err = new FetchError(`Response timeout while trying to fetch ${data.url} (over ${data.timeout}ms)`, 'body-timeout'); reject(err); body.destroy(err); }, data.timeout); } body.on('data', chunk => { if (abort || chunk === null) { return; } if (data.size && accumBytes + chunk.length > data.size) { abort = true; reject(new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size')); return; } accumBytes += chunk.length; accum.push(chunk); }); Stream.finished(body, {writable: false}, err => { clearTimeout(resTimeout); if (err) { if (isAbortError(err)) { // If the request was aborted, reject with this Error abort = true; reject(err); } else { // Other errors, such as incorrect content-encoding reject(new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err)); } } else { if (abort) { return; } try { resolve(Buffer.concat(accum, accumBytes)); } catch (error) { // Handle streams that have accumulated too much data (issue #414) reject(new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error)); } } }); }); }; /** * 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 => { // 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 (isURLSearchParams(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()}`; } // 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 = ({body}) => { // 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 {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); } }; // Expose Promise Body.Promise = global.Promise; /** * Headers.js * * Headers class offers convenient helpers */ const invalidTokenRegex = /[^`\-\w!#$%&'*+.|~]/; const invalidHeaderCharRegex = /[^\t\u0020-\u007E\u0080-\u00FF]/; const validateName = name => { name = `${name}`; if (invalidTokenRegex.test(name) || name === '') { throw new TypeError(`${name} is not a legal HTTP header name`); } }; const validateValue = value => { value = `${value}`; if (invalidHeaderCharRegex.test(value)) { throw new TypeError(`${value} is not a legal HTTP header value`); } }; /** * Find the key in the map object given a header name. * * Returns undefined if not found. * * @param String name Header name * @return String|Undefined */ const find = (map, name) => { name = name.toLowerCase(); for (const key in map) { if (key.toLowerCase() === name) { return key; } } return undefined; }; const MAP = Symbol('map'); class Headers { /** * Headers class * * @param Object headers Response headers * @return Void */ constructor(init = undefined) { this[MAP] = Object.create(null); if (init instanceof Headers) { const rawHeaders = init.raw(); const headerNames = Object.keys(rawHeaders); for (const headerName of headerNames) { for (const value of rawHeaders[headerName]) { this.append(headerName, value); } } return; } // We don't worry about converting prop to ByteString here as append() // will handle it. // eslint-disable-next-line no-eq-null, eqeqeq if (init == null) ; else if (typeof init === 'object') { const method = init[Symbol.iterator]; // eslint-disable-next-line no-eq-null, eqeqeq if (method == null) { // Record for (const key of Object.keys(init)) { const value = init[key]; this.append(key, value); } } 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 const pairs = []; for (const pair of init) { if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') { throw new TypeError('Each header pair must be iterable'); } pairs.push([...pair]); } for (const pair of pairs) { if (pair.length !== 2) { throw new TypeError('Each header pair must be a name/value tuple'); } this.append(pair[0], pair[1]); } } } else { throw new TypeError('Provided initializer must be an object'); } } /** * Return combined header value given name * * @param String name Header name * @return Mixed */ get(name) { name = `${name}`; validateName(name); const key = find(this[MAP], name); if (key === undefined) { return null; } let value = this[MAP][key].join(', '); if (name.toLowerCase() === 'content-encoding') { value = value.toLowerCase(); } return value; } /** * Iterate over all headers * * @param Function callback Executed for each item with parameters (value, name, thisArg) * @param Boolean thisArg `this` context for callback function * @return Void */ forEach(callback, thisArg = undefined) { let pairs = getHeaders(this); let i = 0; while (i < pairs.length) { const [name, value] = pairs[i]; callback.call(thisArg, value, name, this); pairs = getHeaders(this); i++; } } /** * Overwrite header values given name * * @param String name Header name * @param String value Header value * @return Void */ set(name, value) { name = `${name}`; value = `${value}`; validateName(name); validateValue(value); const key = find(this[MAP], name); this[MAP][key === undefined ? name : key] = [value]; } /** * Append a value onto existing header * * @param String name Header name * @param String value Header value * @return Void */ append(name, value) { name = `${name}`; value = `${value}`; validateName(name); validateValue(value); const key = find(this[MAP], name); if (key === undefined) { this[MAP][name] = [value]; } else { this[MAP][key].push(value); } } /** * Check for header name existence * * @param String name Header name * @return Boolean */ has(name) { name = `${name}`; validateName(name); return find(this[MAP], name) !== undefined; } /** * Delete all header values given name * * @param String name Header name * @return Void */ delete(name) { name = `${name}`; validateName(name); const key = find(this[MAP], name); if (key !== undefined) { delete this[MAP][key]; } } /** * Return raw headers (non-spec api) * * @return Object */ raw() { return this[MAP]; } /** * Get an iterator on keys. * * @return Iterator */ keys() { return createHeadersIterator(this, 'key'); } /** * Get an iterator on values. * * @return Iterator */ values() { return createHeadersIterator(this, 'value'); } /** * Get an iterator on entries. * * This is the default iterator of the Headers object. * * @return Iterator */ [Symbol.iterator]() { return createHeadersIterator(this, 'key+value'); } } Headers.prototype.entries = Headers.prototype[Symbol.iterator]; Object.defineProperty(Headers.prototype, Symbol.toStringTag, { value: 'Headers', writable: false, enumerable: false, configurable: true }); Object.defineProperties(Headers.prototype, { get: {enumerable: true}, forEach: {enumerable: true}, set: {enumerable: true}, append: {enumerable: true}, has: {enumerable: true}, delete: {enumerable: true}, keys: {enumerable: true}, values: {enumerable: true}, entries: {enumerable: true} }); const getHeaders = (headers, kind = 'key+value') => { const keys = Object.keys(headers[MAP]).sort(); let iterator; if (kind === 'key') { iterator = header => header.toLowerCase(); } else if (kind === 'value') { iterator = header => headers[MAP][header].join(', '); } else { iterator = header => [header.toLowerCase(), headers[MAP][header].join(', ')]; } return keys.map(header => iterator(header)); }; const INTERNAL = Symbol('internal'); const createHeadersIterator = (target, kind) => { const iterator = Object.create(HeadersIteratorPrototype); iterator[INTERNAL] = { target, kind, index: 0 }; return iterator; }; const HeadersIteratorPrototype = Object.setPrototypeOf({ next() { // istanbul ignore if if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) { throw new TypeError('Value of `this` is not a HeadersIterator'); } const { target, kind, index } = this[INTERNAL]; const values = getHeaders(target, kind); const length_ = values.length; if (index >= length_) { return { value: undefined, done: true }; } this[INTERNAL].index = index + 1; return { value: values[index], done: false }; } }, Object.getPrototypeOf( Object.getPrototypeOf([][Symbol.iterator]()) )); Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, { value: 'HeadersIterator', writable: false, enumerable: false, configurable: true }); /** * Export the Headers object in a form that Node.js can consume. * * @param Headers headers * @return Object */ const exportNodeCompatibleHeaders = headers => { const object = {__proto__: null, ...headers[MAP]}; // Http.request() only supports string as Host header. This hack makes // specifying custom Host header possible. const hostHeaderKey = find(headers[MAP], 'Host'); if (hostHeaderKey !== undefined) { object[hostHeaderKey] = object[hostHeaderKey][0]; } return object; }; /** * Create a Headers object from an object of headers, ignoring those that do * not conform to HTTP grammar productions. * * @param Object obj Object of headers * @return Headers */ const createHeadersLenient = object => { const headers = new Headers(); for (const name of Object.keys(object)) { if (invalidTokenRegex.test(name)) { continue; } if (Array.isArray(object[name])) { for (const value of object[name]) { if (invalidHeaderCharRegex.test(value)) { continue; } if (headers[MAP][name] === undefined) { headers[MAP][name] = [value]; } else { headers[MAP][name].push(value); } } } else if (!invalidHeaderCharRegex.test(object[name])) { headers[MAP][name] = [object[name]]; } } return headers; }; 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, timeout: this.timeout }); } /** * @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' ); }; /** * Wrapper around `new URL` to handle relative URLs (https://github.com/nodejs/node/issues/12682) * * @param {string} urlStr * @return {void} */ const parseURL = urlString => { /* Check whether the URL is absolute or not Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 */ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.exec(urlString)) { return new URL(urlString); } throw new TypeError('Only absolute URLs are supported'); }; /** * 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/bitinn/node-fetch/issues/245) if (isRequest(input)) { parsedURL = parseURL(input.url); } else { if (input && input.href) { // In order to support Node.js' Url objects; though WHATWG's URL objects // will fall into this branch also (since their `toString()` will return // `href` property anyway) parsedURL = parseURL(input.href); } else { // Coerce input to a string before attempting to parse parsedURL = parseURL(`${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, { timeout: init.timeout || input.timeout || 0, 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); 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; } 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', '*/*'); } if (!/^https?:$/.test(parsedURL.protocol)) { throw new TypeError('Only HTTP(S) protocols are supported'); } // 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); if (typeof totalBytes === 'number') { 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/1.0 (+https://github.com/bitinn/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: exportNodeCompatibleHeaders(headers), agent }; return requestOptions; }; /** * Abort-error.js * * AbortError interface for cancelled requests */ /** * Create AbortError instance * * @param String message Error message for human * @param String type Error type for machine * @param String systemError For Node.js system error * @return AbortError */ class AbortError extends Error { constructor(message) { super(message); this.type = 'aborted'; this.message = message; this.name = 'AbortError'; this[Symbol.toStringTag] = 'AbortError'; // Hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); } } /** * 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/. */ /** * Fetch function * * @param Mixed url Absolute url or Request instance * @param Object opts Fetch options * @return Promise */ const fetch = (url, options_) => { // Allow custom promise if (!fetch.Promise) { throw new Error('native promise missing, set fetch.Promise to your favorite alternative'); } // Regex for data uri const dataUriRegex = /^\s*data:([a-z]+\/[a-z]+(;[a-z-]+=[a-z-]+)?)?(;base64)?,[\w!$&',()*+;=\-.~:@/?%\s]*\s*$/i; // If valid data uri if (dataUriRegex.test(url)) { const data = dataURIToBuffer(url); const res = new Response(data, {headers: {'Content-Type': data.type}}); return fetch.Promise.resolve(res); } // If invalid data uri if (url.toString().startsWith('data:')) { const request = new Request(url, options_); return fetch.Promise.reject(new FetchError(`[${request.method}] ${request.url} invalid URL`, 'system')); } Body.Promise = fetch.Promise; // Wrap http.request into fetch return new fetch.Promise((resolve, reject) => { // Build request object const request = new Request(url, options_); const options = getNodeRequestOptions(request); 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); } }; if (request.timeout) { request_.setTimeout(request.timeout, () => { finalize(); reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout')); }); } request_.on('error', err => { reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err)); finalize(); }); request_.on('response', res => { request_.setTimeout(0); const headers = createHeadersLenient(res.headers); // HTTP fetch step 5 if (isRedirect(res.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); } catch (error) { // istanbul ignore next: nodejs server prevent invalid response headers, we can't test this through normal request 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, timeout: request.timeout }; // HTTP-redirect fetch step 9 if (res.statusCode !== 303 && request.body && getTotalBytes(request) === null) { reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); finalize(); return; } // HTTP-redirect fetch step 11 if (res.statusCode === 303 || ((res.statusCode === 301 || res.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 res.once('end', () => { if (signal) { signal.removeEventListener('abort', abortAndFinalize); } }); let body = Stream.pipeline(res, new Stream.PassThrough(), error => { reject(error); }); const responseOptions = { url: request.url, status: res.statusCode, statusText: res.statusMessage, headers, size: request.size, timeout: request.timeout, 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 || res.statusCode === 204 || res.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(res, 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); }); }; // Expose Promise fetch.Promise = global.Promise; 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