/** * Request.js * * Request class contains server only options * * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ import {format as formatUrl} from 'url'; import Headers from './headers.js'; import Body, {clone, extractContentType, getTotalBytes} from './body.js'; import {isAbortSignal} from './utils/is.js'; import {getSearch} from './utils/get-search.js'; const INTERNALS = 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] === 'object' ); }; /** * Request class * * Ref: https://fetch.spec.whatwg.org/#request-class * * @param Mixed input Url or Request instance * @param Object init Custom options * @return Void */ export default 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; } // 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'); } this[INTERNALS] = { 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].method; } get url() { return formatUrl(this[INTERNALS].parsedURL); } get headers() { return this[INTERNALS].headers; } get redirect() { return this[INTERNALS].redirect; } get signal() { return this[INTERNALS].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 */ export 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, query: parsedURL.query, href: parsedURL.href, method: request.method, headers: headers[Symbol.for('nodejs.util.inspect.custom')](), insecureHTTPParser: request.insecureHTTPParser, agent }; return requestOptions; };