/* A general purpose fetch function for ARM resources. Not designed to be used directly Instead, generate ARM clients that consume this function with stricter typing. */ import promiseRetry, { AbortError } from "p-retry"; import { configContext } from "../../ConfigContext"; import { userContext } from "../../UserContext"; interface ErrorResponse { code: string; message: string; } // ARM sometimes returns an error wrapped in a top level error object // Example: 409 Conflict error when trying to delete a locked resource interface WrappedErrorResponse { error: ErrorResponse; } type ParsedErrorResponse = ErrorResponse | WrappedErrorResponse; export class ARMError extends Error { constructor(message: string) { super(message); // Set the prototype explicitly. // https://github.com/Microsoft/TypeScript/wiki/FAQ#why-doesnt-extending-built-ins-like-error-array-and-map-work Object.setPrototypeOf(this, ARMError.prototype); } public code?: string | number; } interface ARMQueryParams { filter?: string; metricNames?: string; } interface Options { host: string; path: string; apiVersion: string; method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"; body?: unknown; queryParams?: ARMQueryParams; } export async function armRequestWithoutPolling({ host, path, apiVersion, method, body: requestBody, queryParams, }: Options): Promise<{ result: T; operationStatusUrl: string }> { const url = new URL(path, host); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); if (queryParams) { queryParams.filter && url.searchParams.append("$filter", queryParams.filter); queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames); } if (!userContext.authorizationToken) { throw new Error("No authority token provided"); } const response = await window.fetch(url.href, { method, headers: { Authorization: userContext.authorizationToken, }, body: requestBody ? JSON.stringify(requestBody) : undefined, }); if (!response.ok) { let error: ARMError; try { const errorResponse = (await response.json()) as ParsedErrorResponse; if ("error" in errorResponse) { error = new ARMError(errorResponse.error.message); error.code = errorResponse.error.code; } else { error = new ARMError(errorResponse.message); error.code = errorResponse.code; } } catch (error) { throw new Error(await response.text()); } throw error; } const operationStatusUrl = (response.headers && response.headers.get("location")) || ""; const responseBody = (await response.json()) as T; return { result: responseBody, operationStatusUrl: operationStatusUrl }; } // TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them. export async function armRequest({ host, path, apiVersion, method, body: requestBody, queryParams, }: Options): Promise { const armRequestResult = await armRequestWithoutPolling({ host, path, apiVersion, method, body: requestBody, queryParams, }); const operationStatusUrl = armRequestResult.operationStatusUrl; if (operationStatusUrl) { return await promiseRetry(() => getOperationStatus(operationStatusUrl)); } return armRequestResult.result; } async function getOperationStatus(operationStatusUrl: string) { if (!userContext.authorizationToken) { throw new Error("No authority token provided"); } const response = await window.fetch(operationStatusUrl, { headers: { Authorization: userContext.authorizationToken, }, }); if (!response.ok) { const errorResponse = (await response.json()) as ErrorResponse; const error = new Error(errorResponse.message) as ARMError; error.code = errorResponse.code; throw new AbortError(error); } if (response.status === 204) { return; } const body = await response.json(); const status = body.status; if (!status && response.status === 200) { return body; } if (status === "Canceled" || status === "Failed") { const errorMessage = body.error ? JSON.stringify(body.error) : "Operation could not be completed"; const error = new Error(errorMessage); throw new AbortError(error); } throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`); }