private async doRequest()

in src/apiv2.ts [297:448]


  private async doRequest<ReqT, ResT>(
    options: InternalClientRequestOptions<ReqT>
  ): Promise<ClientResponse<ResT>> {
    if (!options.path.startsWith("/")) {
      options.path = "/" + options.path;
    }

    let fetchURL = this.requestURL(options);
    if (options.queryParams) {
      if (!(options.queryParams instanceof URLSearchParams)) {
        const sp = new URLSearchParams();
        for (const key of Object.keys(options.queryParams)) {
          const value = options.queryParams[key];
          sp.append(key, `${value}`);
        }
        options.queryParams = sp;
      }
      const queryString = options.queryParams.toString();
      if (queryString) {
        fetchURL += `?${queryString}`;
      }
    }

    const fetchOptions: RequestInit = {
      headers: options.headers,
      method: options.method,
      redirect: options.redirect,
      compress: options.compress,
    };

    if (this.opts.proxy) {
      fetchOptions.agent = new ProxyAgent(this.opts.proxy);
    }
    const envProxy = proxyURIFromEnv();
    if (envProxy) {
      fetchOptions.agent = new ProxyAgent(envProxy);
    }

    if (options.signal) {
      fetchOptions.signal = options.signal;
    }

    let reqTimeout: NodeJS.Timeout | undefined;
    if (options.timeout) {
      const controller = new AbortController();
      reqTimeout = setTimeout(() => {
        controller.abort();
      }, options.timeout);
      fetchOptions.signal = controller.signal;
    }

    if (typeof options.body === "string" || isStream(options.body)) {
      fetchOptions.body = options.body;
    } else if (options.body !== undefined) {
      fetchOptions.body = JSON.stringify(options.body);
    }

    // TODO(bkendall): Refactor this to use Throttler _or_ refactor Throttle to use `retry`.
    const operationOptions: retry.OperationOptions = {
      retries: options.retryCodes?.length ? 1 : 2,
      minTimeout: 1 * 1000,
      maxTimeout: 5 * 1000,
    };
    if (typeof options.retries === "number") {
      operationOptions.retries = options.retries;
    }
    if (typeof options.retryMinTimeout === "number") {
      operationOptions.minTimeout = options.retryMinTimeout;
    }
    if (typeof options.retryMaxTimeout === "number") {
      operationOptions.maxTimeout = options.retryMaxTimeout;
    }
    const operation = retry.operation(operationOptions);

    return await new Promise<ClientResponse<ResT>>((resolve, reject) => {
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
      operation.attempt(async (currentAttempt): Promise<void> => {
        let res: Response;
        let body: ResT;
        try {
          if (currentAttempt > 1) {
            logger.debug(
              `*** [apiv2] Attempting the request again. Attempt number ${currentAttempt}`
            );
          }
          this.logRequest(options);

          try {
            res = await fetch(fetchURL, fetchOptions);
          } catch (thrown: any) {
            const err = thrown instanceof Error ? thrown : new Error(thrown);
            const isAbortError = err.name.includes("AbortError");
            if (isAbortError) {
              throw new FirebaseError(`Timeout reached making request to ${fetchURL}`, {
                original: err,
              });
            }
            throw new FirebaseError(`Failed to make request to ${fetchURL}`, { original: err });
          } finally {
            // If we succeed or failed, clear the timeout.
            if (reqTimeout) {
              clearTimeout(reqTimeout);
            }
          }

          if (options.responseType === "json") {
            const text = await res.text();
            // Some responses, such as 204 and occasionally 202s don't have
            // any content. We can't just rely on response code (202 may have conent)
            // and unfortuantely res.length is unreliable (many requests return zero).
            if (!text.length) {
              body = undefined as unknown as ResT;
            } else {
              try {
                body = JSON.parse(text) as ResT;
              } catch (err: unknown) {
                throw new FirebaseError(`Unable to parse JSON: ${err}`);
              }
            }
          } else if (options.responseType === "stream") {
            body = res.body as unknown as ResT;
          } else {
            throw new FirebaseError(`Unable to interpret response. Please set responseType.`, {
              exit: 2,
            });
          }
        } catch (err: unknown) {
          return err instanceof FirebaseError ? reject(err) : reject(new FirebaseError(`${err}`));
        }

        this.logResponse(res, body, options);

        if (res.status >= 400) {
          if (options.retryCodes?.includes(res.status)) {
            const err = responseToError({ statusCode: res.status }, body) || undefined;
            if (operation.retry(err)) {
              return;
            }
          }
          if (!options.resolveOnHTTPError) {
            return reject(responseToError({ statusCode: res.status }, body));
          }
        }

        resolve({
          status: res.status,
          response: res,
          body,
        });
      });
    });
  }