UNPKG

7.92 kBJavaScriptView Raw
1'use strict';
2/* istanbul ignore next: compatibility reason */
3const URLGlobal = typeof URL === 'undefined' ? require('url').URL : URL; // TODO: Use the `URL` global when targeting Node.js 10
4/* istanbul ignore next: compatibility reason */
5const URLSearchParamsGlobal = typeof URLSearchParams === 'undefined' ? require('url').URLSearchParams : URLSearchParams;
6const is = require('@sindresorhus/is');
7const toReadableStream = require('to-readable-stream');
8const urlParseLax = require('url-parse-lax');
9const isRetryOnNetworkErrorAllowed = require('./is-retry-on-network-error-allowed');
10const urlToOptions = require('./url-to-options');
11const isFormData = require('./is-form-data');
12const knownHookEvents = require('./known-hook-events');
13
14const retryAfterStatusCodes = new Set([413, 429, 503]);
15
16// `preNormalize` handles things related to static options, like `baseUrl`, `followRedirect`, `hooks`, etc.
17// While `normalize` does `preNormalize` + handles things related to dynamic options, like URL, headers, body, etc.
18const preNormalize = options => {
19 options = {
20 headers: {},
21 ...options
22 };
23
24 if (options.baseUrl && !options.baseUrl.toString().endsWith('/')) {
25 options.baseUrl += '/';
26 }
27
28 if (is.undefined(options.followRedirect)) {
29 options.followRedirect = true;
30 }
31
32 if (is.nullOrUndefined(options.hooks)) {
33 options.hooks = {};
34 }
35 if (is.object(options.hooks)) {
36 for (const hookEvent of knownHookEvents) {
37 const hooks = options.hooks[hookEvent];
38 if (is.nullOrUndefined(hooks)) {
39 options.hooks[hookEvent] = [];
40 } else if (is.array(hooks)) {
41 for (const [index, hook] of hooks.entries()) {
42 if (!is.function(hook)) {
43 throw new TypeError(
44 `Parameter \`hooks.${hookEvent}[${index}]\` must be a function, not ${is(hook)}`
45 );
46 }
47 }
48 } else {
49 throw new TypeError(`Parameter \`hooks.${hookEvent}\` must be an array, not ${is(hooks)}`);
50 }
51 }
52 } else {
53 throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`);
54 }
55
56 return options;
57};
58
59module.exports = (url, options, defaults) => {
60 if (Reflect.has(options, 'url') || (is.object(url) && Reflect.has(url, 'url'))) {
61 throw new TypeError('Parameter `url` is not an option. Use got(url, options)');
62 }
63
64 if (!is.string(url) && !is.object(url)) {
65 throw new TypeError(`Parameter \`url\` must be a string or object, not ${is(url)}`);
66 }
67
68 options = preNormalize(options);
69
70 if (is.string(url)) {
71 if (options.baseUrl) {
72 if (url.toString().startsWith('/')) {
73 url = url.toString().slice(1);
74 }
75
76 url = urlToOptions(new URLGlobal(url, options.baseUrl));
77 } else {
78 url = url.replace(/^unix:/, 'http://$&');
79
80 try {
81 decodeURI(url);
82 } catch (_) {
83 throw new Error('Parameter `url` must contain valid UTF-8 character sequences');
84 }
85
86 url = urlParseLax(url);
87 if (url.auth) {
88 throw new Error('Basic authentication must be done with the `auth` option');
89 }
90 }
91 } else if (is(url) === 'URL') {
92 url = urlToOptions(url);
93 }
94
95 options = {
96 path: '',
97 ...url,
98 protocol: url.protocol || 'http:', // Override both null/undefined with default protocol
99 ...options
100 };
101
102 const {baseUrl} = options;
103 Object.defineProperty(options, 'baseUrl', {
104 set: () => {
105 throw new Error('Failed to set baseUrl. Options are normalized already.');
106 },
107 get: () => baseUrl
108 });
109
110 const {query} = options;
111 if (!is.empty(query) || query instanceof URLSearchParamsGlobal) {
112 const queryParams = new URLSearchParamsGlobal(query);
113 options.path = `${options.path.split('?')[0]}?${queryParams.toString()}`;
114 delete options.query;
115 }
116
117 if (options.stream && options.json) {
118 options.json = false;
119 }
120
121 if (options.json && is.undefined(options.headers.accept)) {
122 options.headers.accept = 'application/json';
123 }
124
125 const {headers} = options;
126 for (const [key, value] of Object.entries(headers)) {
127 if (is.nullOrUndefined(value)) {
128 delete headers[key];
129 }
130 }
131
132 const {body} = options;
133 if (is.nullOrUndefined(body)) {
134 options.method = options.method || 'GET';
135 } else {
136 const isObject = is.object(body) && !Buffer.isBuffer(body) && !is.nodeStream(body);
137 if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body) && !(options.form || options.json)) {
138 throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
139 }
140
141 if (options.json && !(isObject || is.array(body))) {
142 throw new TypeError('The `body` option must be an Object or Array when the `json` option is used');
143 }
144
145 if (options.form && !isObject) {
146 throw new TypeError('The `body` option must be an Object when the `form` option is used');
147 }
148
149 if (isFormData(body)) {
150 // Special case for https://github.com/form-data/form-data
151 headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`;
152 } else if (options.form) {
153 headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded';
154 options.body = (new URLSearchParamsGlobal(body)).toString();
155 } else if (options.json) {
156 headers['content-type'] = headers['content-type'] || 'application/json';
157 options.body = JSON.stringify(body);
158 }
159
160 // Convert buffer to stream to receive upload progress events (#322)
161 if (is.buffer(body)) {
162 options.body = toReadableStream(body);
163 options.body._buffer = body;
164 }
165
166 options.method = options.method || 'POST';
167 }
168
169 options.method = options.method.toUpperCase();
170
171 if (options.decompress && is.undefined(options.headers['accept-encoding'])) {
172 options.headers['accept-encoding'] = 'gzip, deflate';
173 }
174
175 if (options.hostname === 'unix') {
176 const matches = /(.+?):(.+)/.exec(options.path);
177
178 if (matches) {
179 const [, socketPath, path] = matches;
180 options = {
181 ...options,
182 socketPath,
183 path,
184 host: null
185 };
186 }
187 }
188
189 options.gotRetry = {retries: 0, methods: [], statusCodes: []};
190 if (options.retry !== false) {
191 if (is.number(options.retry)) {
192 if (is.object(defaults.options.retry)) {
193 options.gotRetry = {...defaults.options.retry, retries: options.retry};
194 } else {
195 options.gotRetry.retries = options.retry;
196 }
197 } else {
198 options.gotRetry = {...options.gotRetry, ...options.retry};
199 }
200 delete options.retry;
201 }
202
203 options.gotRetry.methods = new Set(options.gotRetry.methods.map(method => method.toUpperCase()));
204 options.gotRetry.statusCodes = new Set(options.gotRetry.statusCodes);
205
206 if (!options.gotRetry.maxRetryAfter && Reflect.has(options, 'timeout')) {
207 if (is.number(options.timeout)) {
208 options.gotRetry.maxRetryAfter = options.timeout;
209 } else {
210 options.gotRetry.maxRetryAfter = Math.min(...[options.timeout.request, options.timeout.connection].filter(n => !is.nullOrUndefined(n)));
211 }
212 }
213
214 if (is.number(options.timeout) || is.object(options.timeout)) {
215 if (is.number(options.timeout)) {
216 options.gotTimeout = {request: options.timeout};
217 } else {
218 options.gotTimeout = options.timeout;
219 }
220 delete options.timeout;
221 }
222
223 if (!is.function(options.gotRetry.retries)) {
224 const {retries} = options.gotRetry;
225
226 options.gotRetry.retries = (iteration, error) => {
227 if (iteration > retries || (!isRetryOnNetworkErrorAllowed(error) && (!options.gotRetry.methods.has(error.method) || !options.gotRetry.statusCodes.has(error.statusCode)))) {
228 return 0;
229 }
230
231 if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
232 let after = Number(error.headers['retry-after']);
233 if (is.nan(after)) {
234 after = Date.parse(error.headers['retry-after']) - Date.now();
235 } else {
236 after *= 1000;
237 }
238
239 if (after > options.gotRetry.maxRetryAfter) {
240 return 0;
241 }
242
243 return after;
244 }
245
246 if (error.statusCode === 413) {
247 return 0;
248 }
249
250 const noise = Math.random() * 100;
251
252 return ((1 << iteration) * 1000) + noise;
253 };
254 }
255
256 return options;
257};
258
259module.exports.preNormalize = preNormalize;