Spaces:
Sleeping
Sleeping
/** | |
* 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 'node:url'; | |
import {deprecate} from 'node:util'; | |
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'; | |
import { | |
validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY | |
} from './utils/referrer.js'; | |
const INTERNALS = Symbol('Request internals'); | |
/** | |
* Check if `obj` is an instance of Request. | |
* | |
* @param {*} object | |
* @return {boolean} | |
*/ | |
const isRequest = object => { | |
return ( | |
typeof object === 'object' && | |
typeof object[INTERNALS] === 'object' | |
); | |
}; | |
const doBadDataWarn = deprecate(() => {}, | |
'.data is not a valid RequestInit property, use .body instead', | |
'https://github.com/node-fetch/node-fetch/issues/1000 (request)'); | |
/** | |
* 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 = {}; | |
} | |
if (parsedURL.username !== '' || parsedURL.password !== '') { | |
throw new TypeError(`${parsedURL} is an url with embedded credentials.`); | |
} | |
let method = init.method || input.method || 'GET'; | |
if (/^(delete|get|head|options|post|put)$/i.test(method)) { | |
method = method.toUpperCase(); | |
} | |
if (!isRequest(init) && 'data' in init) { | |
doBadDataWarn(); | |
} | |
// 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.set('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'); | |
} | |
// §5.4, Request constructor steps, step 15.1 | |
// eslint-disable-next-line no-eq-null, eqeqeq | |
let referrer = init.referrer == null ? input.referrer : init.referrer; | |
if (referrer === '') { | |
// §5.4, Request constructor steps, step 15.2 | |
referrer = 'no-referrer'; | |
} else if (referrer) { | |
// §5.4, Request constructor steps, step 15.3.1, 15.3.2 | |
const parsedReferrer = new URL(referrer); | |
// §5.4, Request constructor steps, step 15.3.3, 15.3.4 | |
referrer = /^about:(\/\/)?client$/.test(parsedReferrer) ? 'client' : parsedReferrer; | |
} else { | |
referrer = undefined; | |
} | |
this[INTERNALS] = { | |
method, | |
redirect: init.redirect || input.redirect || 'follow', | |
headers, | |
parsedURL, | |
signal, | |
referrer | |
}; | |
// 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; | |
// §5.4, Request constructor steps, step 16. | |
// Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy | |
this.referrerPolicy = init.referrerPolicy || input.referrerPolicy || ''; | |
} | |
/** @returns {string} */ | |
get method() { | |
return this[INTERNALS].method; | |
} | |
/** @returns {string} */ | |
get url() { | |
return formatUrl(this[INTERNALS].parsedURL); | |
} | |
/** @returns {Headers} */ | |
get headers() { | |
return this[INTERNALS].headers; | |
} | |
get redirect() { | |
return this[INTERNALS].redirect; | |
} | |
/** @returns {AbortSignal} */ | |
get signal() { | |
return this[INTERNALS].signal; | |
} | |
// https://fetch.spec.whatwg.org/#dom-request-referrer | |
get referrer() { | |
if (this[INTERNALS].referrer === 'no-referrer') { | |
return ''; | |
} | |
if (this[INTERNALS].referrer === 'client') { | |
return 'about:client'; | |
} | |
if (this[INTERNALS].referrer) { | |
return this[INTERNALS].referrer.toString(); | |
} | |
return undefined; | |
} | |
get referrerPolicy() { | |
return this[INTERNALS].referrerPolicy; | |
} | |
set referrerPolicy(referrerPolicy) { | |
this[INTERNALS].referrerPolicy = validateReferrerPolicy(referrerPolicy); | |
} | |
/** | |
* 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}, | |
referrer: {enumerable: true}, | |
referrerPolicy: {enumerable: true} | |
}); | |
/** | |
* Convert a Request to Node.js http request options. | |
* | |
* @param {Request} request - A Request instance | |
* @return 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); | |
} | |
// 4.1. Main fetch, step 2.6 | |
// > If request's referrer policy is the empty string, then set request's referrer policy to the | |
// > default referrer policy. | |
if (request.referrerPolicy === '') { | |
request.referrerPolicy = DEFAULT_REFERRER_POLICY; | |
} | |
// 4.1. Main fetch, step 2.7 | |
// > If request's referrer is not "no-referrer", set request's referrer to the result of invoking | |
// > determine request's referrer. | |
if (request.referrer && request.referrer !== 'no-referrer') { | |
request[INTERNALS].referrer = determineRequestsReferrer(request); | |
} else { | |
request[INTERNALS].referrer = 'no-referrer'; | |
} | |
// 4.5. HTTP-network-or-cache fetch, step 6.9 | |
// > If httpRequest's referrer is a URL, then append `Referer`/httpRequest's referrer, serialized | |
// > and isomorphic encoded, to httpRequest's header list. | |
if (request[INTERNALS].referrer instanceof URL) { | |
headers.set('Referer', request.referrer); | |
} | |
// 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); | |
} | |
// HTTP-network fetch step 4.2 | |
// chunked encoding is handled by Node.js | |
const search = getSearch(parsedURL); | |
// Pass the full URL directly to request(), but overwrite the following | |
// options: | |
const options = { | |
// Overwrite search to retain trailing ? (issue #776) | |
path: parsedURL.pathname + search, | |
// The following options are not expressed in the URL | |
method: request.method, | |
headers: headers[Symbol.for('nodejs.util.inspect.custom')](), | |
insecureHTTPParser: request.insecureHTTPParser, | |
agent | |
}; | |
return { | |
/** @type {URL} */ | |
parsedURL, | |
options | |
}; | |
}; | |