function fetchJson()

in addons/addon-base-ui/packages/base-ui/src/helpers/api.js [55:166]


function fetchJson(url, options = {}, retryCount = 0) {
  // see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  let isOk = false;
  let httpStatus;

  const headers = {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
  };
  const body = '';
  const merged = {
    method: 'GET',
    cache: 'no-cache',
    mode: config.fetchMode,
    redirect: 'follow',
    body,
    ...options,
    headers: { ...headers, ...options.headers },
  };

  if (merged.method === 'GET') delete merged.body; // otherwise fetch will throw an error

  let retryOptions = options;
  if (merged.params) {
    // if query string parameters are specified then add them to the URL
    // The merged.params here is just a plain JavaScript object with key and value
    // For example {key1: value1, key2: value2}

    // Get keys from the params object such as [key1, key2] etc
    const paramKeys = _.keys(merged.params);

    // Filter out params with undefined or null values
    const paramKeysToPass = _.filter(paramKeys, key => !_.isNil(_.get(merged.params, key)));
    const query = _.map(
      paramKeysToPass,
      key => `${encodeURIComponent(key)}=${encodeURIComponent(_.get(merged.params, key))}`,
    ).join('&');
    url = query ? `${url}?${query}` : url;

    // Omit options.params after they are added to the url as query string params
    // This is required otherwise, if the call fails for some reason (e.g., time out) the same query string params
    // will be added once again to the URL causing duplicate params being passed in.
    // For example, if the options.params = { param1: 'value1', param2: 'value2' }
    // The url will become something like `https://some-host/some-path?param1=value1&param2=value2`
    // If we do not omit "options.params" here and if the call is retried (with a recursive call to "fetchJson") due
    // to timeout or any other issue, the url will then become
    // `https://some-host/some-path?param1=value1&param2=value2?param1=value1&param2=value2`
    retryOptions = _.omit(options, 'params');
  }

  return Promise.resolve()
    .then(() => fetch(url, merged))
    .catch(err => {
      // this will capture network/timeout errors, because fetch does not consider http Status 5xx or 4xx as errors
      if (retryCount < config.maxRetryCount) {
        let backoff = retryCount * retryCount;
        if (backoff < 1) backoff = 1;

        return Promise.resolve()
          .then(() => console.log(`Retrying count = ${retryCount}, Backoff = ${backoff}`))
          .then(() => delay(backoff))
          .then(() => fetchJson(url, retryOptions, retryCount + 1));
      }
      throw parseError(err);
    })
    .then(response => {
      isOk = response.ok;
      httpStatus = response.status;
      return response;
    })
    .then(response => {
      if (_.isFunction(response.text)) return response.text();
      return response;
    })
    .then(text => {
      let json;
      try {
        if (_.isObject(text)) {
          json = text;
        } else {
          json = JSON.parse(text);
        }
      } catch (err) {
        if (httpStatus >= 400) {
          if (httpStatus >= 501 && retryCount < config.maxRetryCount) {
            let backoff = retryCount * retryCount;
            if (backoff < 1) backoff = 1;

            return Promise.resolve()
              .then(() => console.log(`Retrying count = ${retryCount}, Backoff = ${backoff}`))
              .then(() => delay(backoff))
              .then(() => fetchJson(url, retryOptions, retryCount + 1));
          }
          throw parseError({
            message: text,
            status: httpStatus,
          });
        } else {
          throw parseError(new Error('The server did not return a json response.'));
        }
      }

      return json;
    })
    .then(json => {
      if (_.isBoolean(isOk) && !isOk) {
        throw parseError({ ...json, status: httpStatus });
      } else {
        return json;
      }
    });
}