node-fetch/src/request.js

228 lines
5.6 KiB
JavaScript

/**
* 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;
};