UNPKG

8.71 kBJavaScriptView Raw
1/**
2 * Request.js
3 *
4 * Request class contains server only options
5 *
6 * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
7 */
8
9import {format as formatUrl} from 'node:url';
10import {deprecate} from 'node:util';
11import Headers from './headers.js';
12import Body, {clone, extractContentType, getTotalBytes} from './body.js';
13import {isAbortSignal} from './utils/is.js';
14import {getSearch} from './utils/get-search.js';
15import {
16 validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY
17} from './utils/referrer.js';
18
19const INTERNALS = Symbol('Request internals');
20
21/**
22 * Check if `obj` is an instance of Request.
23 *
24 * @param {*} object
25 * @return {boolean}
26 */
27const isRequest = object => {
28 return (
29 typeof object === 'object' &&
30 typeof object[INTERNALS] === 'object'
31 );
32};
33
34const doBadDataWarn = deprecate(() => {},
35 '.data is not a valid RequestInit property, use .body instead',
36 'https://github.com/node-fetch/node-fetch/issues/1000 (request)');
37
38/**
39 * Request class
40 *
41 * Ref: https://fetch.spec.whatwg.org/#request-class
42 *
43 * @param Mixed input Url or Request instance
44 * @param Object init Custom options
45 * @return Void
46 */
47export default class Request extends Body {
48 constructor(input, init = {}) {
49 let parsedURL;
50
51 // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245)
52 if (isRequest(input)) {
53 parsedURL = new URL(input.url);
54 } else {
55 parsedURL = new URL(input);
56 input = {};
57 }
58
59 if (parsedURL.username !== '' || parsedURL.password !== '') {
60 throw new TypeError(`${parsedURL} is an url with embedded credentials.`);
61 }
62
63 let method = init.method || input.method || 'GET';
64 if (/^(delete|get|head|options|post|put)$/i.test(method)) {
65 method = method.toUpperCase();
66 }
67
68 if (!isRequest(init) && 'data' in init) {
69 doBadDataWarn();
70 }
71
72 // eslint-disable-next-line no-eq-null, eqeqeq
73 if ((init.body != null || (isRequest(input) && input.body !== null)) &&
74 (method === 'GET' || method === 'HEAD')) {
75 throw new TypeError('Request with GET/HEAD method cannot have body');
76 }
77
78 const inputBody = init.body ?
79 init.body :
80 (isRequest(input) && input.body !== null ?
81 clone(input) :
82 null);
83
84 super(inputBody, {
85 size: init.size || input.size || 0
86 });
87
88 const headers = new Headers(init.headers || input.headers || {});
89
90 if (inputBody !== null && !headers.has('Content-Type')) {
91 const contentType = extractContentType(inputBody, this);
92 if (contentType) {
93 headers.set('Content-Type', contentType);
94 }
95 }
96
97 let signal = isRequest(input) ?
98 input.signal :
99 null;
100 if ('signal' in init) {
101 signal = init.signal;
102 }
103
104 // eslint-disable-next-line no-eq-null, eqeqeq
105 if (signal != null && !isAbortSignal(signal)) {
106 throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget');
107 }
108
109 // §5.4, Request constructor steps, step 15.1
110 // eslint-disable-next-line no-eq-null, eqeqeq
111 let referrer = init.referrer == null ? input.referrer : init.referrer;
112 if (referrer === '') {
113 // §5.4, Request constructor steps, step 15.2
114 referrer = 'no-referrer';
115 } else if (referrer) {
116 // §5.4, Request constructor steps, step 15.3.1, 15.3.2
117 const parsedReferrer = new URL(referrer);
118 // §5.4, Request constructor steps, step 15.3.3, 15.3.4
119 referrer = /^about:(\/\/)?client$/.test(parsedReferrer) ? 'client' : parsedReferrer;
120 } else {
121 referrer = undefined;
122 }
123
124 this[INTERNALS] = {
125 method,
126 redirect: init.redirect || input.redirect || 'follow',
127 headers,
128 parsedURL,
129 signal,
130 referrer
131 };
132
133 // Node-fetch-only options
134 this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow;
135 this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress;
136 this.counter = init.counter || input.counter || 0;
137 this.agent = init.agent || input.agent;
138 this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384;
139 this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false;
140
141 // §5.4, Request constructor steps, step 16.
142 // Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy
143 this.referrerPolicy = init.referrerPolicy || input.referrerPolicy || '';
144 }
145
146 /** @returns {string} */
147 get method() {
148 return this[INTERNALS].method;
149 }
150
151 /** @returns {string} */
152 get url() {
153 return formatUrl(this[INTERNALS].parsedURL);
154 }
155
156 /** @returns {Headers} */
157 get headers() {
158 return this[INTERNALS].headers;
159 }
160
161 get redirect() {
162 return this[INTERNALS].redirect;
163 }
164
165 /** @returns {AbortSignal} */
166 get signal() {
167 return this[INTERNALS].signal;
168 }
169
170 // https://fetch.spec.whatwg.org/#dom-request-referrer
171 get referrer() {
172 if (this[INTERNALS].referrer === 'no-referrer') {
173 return '';
174 }
175
176 if (this[INTERNALS].referrer === 'client') {
177 return 'about:client';
178 }
179
180 if (this[INTERNALS].referrer) {
181 return this[INTERNALS].referrer.toString();
182 }
183
184 return undefined;
185 }
186
187 get referrerPolicy() {
188 return this[INTERNALS].referrerPolicy;
189 }
190
191 set referrerPolicy(referrerPolicy) {
192 this[INTERNALS].referrerPolicy = validateReferrerPolicy(referrerPolicy);
193 }
194
195 /**
196 * Clone this request
197 *
198 * @return Request
199 */
200 clone() {
201 return new Request(this);
202 }
203
204 get [Symbol.toStringTag]() {
205 return 'Request';
206 }
207}
208
209Object.defineProperties(Request.prototype, {
210 method: {enumerable: true},
211 url: {enumerable: true},
212 headers: {enumerable: true},
213 redirect: {enumerable: true},
214 clone: {enumerable: true},
215 signal: {enumerable: true},
216 referrer: {enumerable: true},
217 referrerPolicy: {enumerable: true}
218});
219
220/**
221 * Convert a Request to Node.js http request options.
222 *
223 * @param {Request} request - A Request instance
224 * @return The options object to be passed to http.request
225 */
226export const getNodeRequestOptions = request => {
227 const {parsedURL} = request[INTERNALS];
228 const headers = new Headers(request[INTERNALS].headers);
229
230 // Fetch step 1.3
231 if (!headers.has('Accept')) {
232 headers.set('Accept', '*/*');
233 }
234
235 // HTTP-network-or-cache fetch steps 2.4-2.7
236 let contentLengthValue = null;
237 if (request.body === null && /^(post|put)$/i.test(request.method)) {
238 contentLengthValue = '0';
239 }
240
241 if (request.body !== null) {
242 const totalBytes = getTotalBytes(request);
243 // Set Content-Length if totalBytes is a number (that is not NaN)
244 if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
245 contentLengthValue = String(totalBytes);
246 }
247 }
248
249 if (contentLengthValue) {
250 headers.set('Content-Length', contentLengthValue);
251 }
252
253 // 4.1. Main fetch, step 2.6
254 // > If request's referrer policy is the empty string, then set request's referrer policy to the
255 // > default referrer policy.
256 if (request.referrerPolicy === '') {
257 request.referrerPolicy = DEFAULT_REFERRER_POLICY;
258 }
259
260 // 4.1. Main fetch, step 2.7
261 // > If request's referrer is not "no-referrer", set request's referrer to the result of invoking
262 // > determine request's referrer.
263 if (request.referrer && request.referrer !== 'no-referrer') {
264 request[INTERNALS].referrer = determineRequestsReferrer(request);
265 } else {
266 request[INTERNALS].referrer = 'no-referrer';
267 }
268
269 // 4.5. HTTP-network-or-cache fetch, step 6.9
270 // > If httpRequest's referrer is a URL, then append `Referer`/httpRequest's referrer, serialized
271 // > and isomorphic encoded, to httpRequest's header list.
272 if (request[INTERNALS].referrer instanceof URL) {
273 headers.set('Referer', request.referrer);
274 }
275
276 // HTTP-network-or-cache fetch step 2.11
277 if (!headers.has('User-Agent')) {
278 headers.set('User-Agent', 'node-fetch');
279 }
280
281 // HTTP-network-or-cache fetch step 2.15
282 if (request.compress && !headers.has('Accept-Encoding')) {
283 headers.set('Accept-Encoding', 'gzip, deflate, br');
284 }
285
286 let {agent} = request;
287 if (typeof agent === 'function') {
288 agent = agent(parsedURL);
289 }
290
291 if (!headers.has('Connection') && !agent) {
292 headers.set('Connection', 'close');
293 }
294
295 // HTTP-network fetch step 4.2
296 // chunked encoding is handled by Node.js
297
298 const search = getSearch(parsedURL);
299
300 // Pass the full URL directly to request(), but overwrite the following
301 // options:
302 const options = {
303 // Overwrite search to retain trailing ? (issue #776)
304 path: parsedURL.pathname + search,
305 // The following options are not expressed in the URL
306 method: request.method,
307 headers: headers[Symbol.for('nodejs.util.inspect.custom')](),
308 insecureHTTPParser: request.insecureHTTPParser,
309 agent
310 };
311
312 return {
313 /** @type {URL} */
314 parsedURL,
315 options
316 };
317};