normalize-arguments.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. 'use strict';
  2. const {URL, URLSearchParams} = require('url'); // TODO: Use the `URL` global when targeting Node.js 10
  3. const urlLib = require('url');
  4. const is = require('@sindresorhus/is');
  5. const urlParseLax = require('url-parse-lax');
  6. const lowercaseKeys = require('lowercase-keys');
  7. const urlToOptions = require('./utils/url-to-options');
  8. const isFormData = require('./utils/is-form-data');
  9. const merge = require('./merge');
  10. const knownHookEvents = require('./known-hook-events');
  11. const retryAfterStatusCodes = new Set([413, 429, 503]);
  12. // `preNormalize` handles static options (e.g. headers).
  13. // For example, when you create a custom instance and make a request
  14. // with no static changes, they won't be normalized again.
  15. //
  16. // `normalize` operates on dynamic options - they cannot be saved.
  17. // For example, `body` is everytime different per request.
  18. // When it's done normalizing the new options, it performs merge()
  19. // on the prenormalized options and the normalized ones.
  20. const preNormalize = (options, defaults) => {
  21. if (is.nullOrUndefined(options.headers)) {
  22. options.headers = {};
  23. } else {
  24. options.headers = lowercaseKeys(options.headers);
  25. }
  26. if (options.baseUrl && !options.baseUrl.toString().endsWith('/')) {
  27. options.baseUrl += '/';
  28. }
  29. if (options.stream) {
  30. options.json = false;
  31. }
  32. if (is.nullOrUndefined(options.hooks)) {
  33. options.hooks = {};
  34. } else if (!is.object(options.hooks)) {
  35. throw new TypeError(`Parameter \`hooks\` must be an object, not ${is(options.hooks)}`);
  36. }
  37. for (const event of knownHookEvents) {
  38. if (is.nullOrUndefined(options.hooks[event])) {
  39. if (defaults) {
  40. options.hooks[event] = [...defaults.hooks[event]];
  41. } else {
  42. options.hooks[event] = [];
  43. }
  44. }
  45. }
  46. if (is.number(options.timeout)) {
  47. options.gotTimeout = {request: options.timeout};
  48. } else if (is.object(options.timeout)) {
  49. options.gotTimeout = options.timeout;
  50. }
  51. delete options.timeout;
  52. const {retry} = options;
  53. options.retry = {
  54. retries: 0,
  55. methods: [],
  56. statusCodes: [],
  57. errorCodes: []
  58. };
  59. if (is.nonEmptyObject(defaults) && retry !== false) {
  60. options.retry = {...defaults.retry};
  61. }
  62. if (retry !== false) {
  63. if (is.number(retry)) {
  64. options.retry.retries = retry;
  65. } else {
  66. options.retry = {...options.retry, ...retry};
  67. }
  68. }
  69. if (options.gotTimeout) {
  70. options.retry.maxRetryAfter = Math.min(...[options.gotTimeout.request, options.gotTimeout.connection].filter(n => !is.nullOrUndefined(n)));
  71. }
  72. if (is.array(options.retry.methods)) {
  73. options.retry.methods = new Set(options.retry.methods.map(method => method.toUpperCase()));
  74. }
  75. if (is.array(options.retry.statusCodes)) {
  76. options.retry.statusCodes = new Set(options.retry.statusCodes);
  77. }
  78. if (is.array(options.retry.errorCodes)) {
  79. options.retry.errorCodes = new Set(options.retry.errorCodes);
  80. }
  81. return options;
  82. };
  83. const normalize = (url, options, defaults) => {
  84. if (is.plainObject(url)) {
  85. options = {...url, ...options};
  86. url = options.url || {};
  87. delete options.url;
  88. }
  89. if (defaults) {
  90. options = merge({}, defaults.options, options ? preNormalize(options, defaults.options) : {});
  91. } else {
  92. options = merge({}, preNormalize(options));
  93. }
  94. if (!is.string(url) && !is.object(url)) {
  95. throw new TypeError(`Parameter \`url\` must be a string or object, not ${is(url)}`);
  96. }
  97. if (is.string(url)) {
  98. if (options.baseUrl) {
  99. if (url.toString().startsWith('/')) {
  100. url = url.toString().slice(1);
  101. }
  102. url = urlToOptions(new URL(url, options.baseUrl));
  103. } else {
  104. url = url.replace(/^unix:/, 'http://$&');
  105. url = urlParseLax(url);
  106. }
  107. } else if (is(url) === 'URL') {
  108. url = urlToOptions(url);
  109. }
  110. // Override both null/undefined with default protocol
  111. options = merge({path: ''}, url, {protocol: url.protocol || 'https:'}, options);
  112. for (const hook of options.hooks.init) {
  113. const called = hook(options);
  114. if (is.promise(called)) {
  115. throw new TypeError('The `init` hook must be a synchronous function');
  116. }
  117. }
  118. const {baseUrl} = options;
  119. Object.defineProperty(options, 'baseUrl', {
  120. set: () => {
  121. throw new Error('Failed to set baseUrl. Options are normalized already.');
  122. },
  123. get: () => baseUrl
  124. });
  125. const {query} = options;
  126. if (is.nonEmptyString(query) || is.nonEmptyObject(query) || query instanceof URLSearchParams) {
  127. if (!is.string(query)) {
  128. options.query = (new URLSearchParams(query)).toString();
  129. }
  130. options.path = `${options.path.split('?')[0]}?${options.query}`;
  131. delete options.query;
  132. }
  133. if (options.hostname === 'unix') {
  134. const matches = /(.+?):(.+)/.exec(options.path);
  135. if (matches) {
  136. const [, socketPath, path] = matches;
  137. options = {
  138. ...options,
  139. socketPath,
  140. path,
  141. host: null
  142. };
  143. }
  144. }
  145. const {headers} = options;
  146. for (const [key, value] of Object.entries(headers)) {
  147. if (is.nullOrUndefined(value)) {
  148. delete headers[key];
  149. }
  150. }
  151. if (options.json && is.undefined(headers.accept)) {
  152. headers.accept = 'application/json';
  153. }
  154. if (options.decompress && is.undefined(headers['accept-encoding'])) {
  155. headers['accept-encoding'] = 'gzip, deflate';
  156. }
  157. const {body} = options;
  158. if (is.nullOrUndefined(body)) {
  159. options.method = options.method ? options.method.toUpperCase() : 'GET';
  160. } else {
  161. const isObject = is.object(body) && !is.buffer(body) && !is.nodeStream(body);
  162. if (!is.nodeStream(body) && !is.string(body) && !is.buffer(body) && !(options.form || options.json)) {
  163. throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
  164. }
  165. if (options.json && !(isObject || is.array(body))) {
  166. throw new TypeError('The `body` option must be an Object or Array when the `json` option is used');
  167. }
  168. if (options.form && !isObject) {
  169. throw new TypeError('The `body` option must be an Object when the `form` option is used');
  170. }
  171. if (isFormData(body)) {
  172. // Special case for https://github.com/form-data/form-data
  173. headers['content-type'] = headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`;
  174. } else if (options.form) {
  175. headers['content-type'] = headers['content-type'] || 'application/x-www-form-urlencoded';
  176. options.body = (new URLSearchParams(body)).toString();
  177. } else if (options.json) {
  178. headers['content-type'] = headers['content-type'] || 'application/json';
  179. options.body = JSON.stringify(body);
  180. }
  181. options.method = options.method ? options.method.toUpperCase() : 'POST';
  182. }
  183. if (!is.function(options.retry.retries)) {
  184. const {retries} = options.retry;
  185. options.retry.retries = (iteration, error) => {
  186. if (iteration > retries) {
  187. return 0;
  188. }
  189. if ((!error || !options.retry.errorCodes.has(error.code)) && (!options.retry.methods.has(error.method) || !options.retry.statusCodes.has(error.statusCode))) {
  190. return 0;
  191. }
  192. if (Reflect.has(error, 'headers') && Reflect.has(error.headers, 'retry-after') && retryAfterStatusCodes.has(error.statusCode)) {
  193. let after = Number(error.headers['retry-after']);
  194. if (is.nan(after)) {
  195. after = Date.parse(error.headers['retry-after']) - Date.now();
  196. } else {
  197. after *= 1000;
  198. }
  199. if (after > options.retry.maxRetryAfter) {
  200. return 0;
  201. }
  202. return after;
  203. }
  204. if (error.statusCode === 413) {
  205. return 0;
  206. }
  207. const noise = Math.random() * 100;
  208. return ((2 ** (iteration - 1)) * 1000) + noise;
  209. };
  210. }
  211. return options;
  212. };
  213. const reNormalize = options => normalize(urlLib.format(options), options);
  214. module.exports = normalize;
  215. module.exports.preNormalize = preNormalize;
  216. module.exports.reNormalize = reNormalize;