Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

250 lines
7.1 KiB

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