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,
});
});
});
}