fetchWithRetries.js.flow 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /**
  2. * Copyright (c) 2013-present, Facebook, Inc.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @providesModule fetchWithRetries
  8. * @typechecks
  9. * @flow
  10. */
  11. 'use strict';
  12. const ExecutionEnvironment = require("./ExecutionEnvironment");
  13. const sprintf = require("./sprintf");
  14. const fetch = require("./fetch");
  15. const warning = require("./warning");
  16. export type InitWithRetries = {
  17. body?: mixed,
  18. cache?: ?string,
  19. credentials?: ?string,
  20. fetchTimeout?: ?number,
  21. headers?: mixed,
  22. method?: ?string,
  23. mode?: ?string,
  24. retryDelays?: ?Array<number>,
  25. };
  26. const DEFAULT_TIMEOUT = 15000;
  27. const DEFAULT_RETRIES = [1000, 3000];
  28. /**
  29. * Makes a POST request to the server with the given data as the payload.
  30. * Automatic retries are done based on the values in `retryDelays`.
  31. */
  32. function fetchWithRetries(uri: string, initWithRetries?: ?InitWithRetries): Promise<any> {
  33. const {
  34. fetchTimeout,
  35. retryDelays,
  36. ...init
  37. } = initWithRetries || {};
  38. const _fetchTimeout = fetchTimeout != null ? fetchTimeout : DEFAULT_TIMEOUT;
  39. const _retryDelays = retryDelays != null ? retryDelays : DEFAULT_RETRIES;
  40. let requestsAttempted = 0;
  41. let requestStartTime = 0;
  42. return new Promise((resolve, reject) => {
  43. /**
  44. * Sends a request to the server that will timeout after `fetchTimeout`.
  45. * If the request fails or times out a new request might be scheduled.
  46. */
  47. function sendTimedRequest(): void {
  48. requestsAttempted++;
  49. requestStartTime = Date.now();
  50. let isRequestAlive = true;
  51. const request = fetch(uri, init);
  52. const requestTimeout = setTimeout(() => {
  53. isRequestAlive = false;
  54. if (shouldRetry(requestsAttempted)) {
  55. warning(false, 'fetchWithRetries: HTTP timeout, retrying.');
  56. retryRequest();
  57. } else {
  58. reject(new Error(sprintf('fetchWithRetries(): Failed to get response from server, ' + 'tried %s times.', requestsAttempted)));
  59. }
  60. }, _fetchTimeout);
  61. request.then(response => {
  62. clearTimeout(requestTimeout);
  63. if (isRequestAlive) {
  64. // We got a response, we can clear the timeout.
  65. if (response.status >= 200 && response.status < 300) {
  66. // Got a response code that indicates success, resolve the promise.
  67. resolve(response);
  68. } else if (shouldRetry(requestsAttempted)) {
  69. // Fetch was not successful, retrying.
  70. // TODO(#7595849): Only retry on transient HTTP errors.
  71. warning(false, 'fetchWithRetries: HTTP error, retrying.'), retryRequest();
  72. } else {
  73. // Request was not successful, giving up.
  74. const error: any = new Error(sprintf('fetchWithRetries(): Still no successful response after ' + '%s retries, giving up.', requestsAttempted));
  75. error.response = response;
  76. reject(error);
  77. }
  78. }
  79. }).catch(error => {
  80. clearTimeout(requestTimeout);
  81. if (shouldRetry(requestsAttempted)) {
  82. retryRequest();
  83. } else {
  84. reject(error);
  85. }
  86. });
  87. }
  88. /**
  89. * Schedules another run of sendTimedRequest based on how much time has
  90. * passed between the time the last request was sent and now.
  91. */
  92. function retryRequest(): void {
  93. const retryDelay = _retryDelays[requestsAttempted - 1];
  94. const retryStartTime = requestStartTime + retryDelay; // Schedule retry for a configured duration after last request started.
  95. setTimeout(sendTimedRequest, retryStartTime - Date.now());
  96. }
  97. /**
  98. * Checks if another attempt should be done to send a request to the server.
  99. */
  100. function shouldRetry(attempt: number): boolean {
  101. return ExecutionEnvironment.canUseDOM && attempt <= _retryDelays.length;
  102. }
  103. sendTimedRequest();
  104. });
  105. }
  106. module.exports = fetchWithRetries;