UNPKG

@azure/ms-rest-azure-js

Version:

Isomorphic Azure client runtime for Typescript/node.js/browser javascript client libraries generated using AutoRest

599 lines (537 loc) 25.1 kB
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. import { delay, HttpMethods, HttpOperationResponse, RequestOptionsBase, RestError, stripRequest, WebResource, OperationResponse, OperationSpec } from "@azure/ms-rest-js"; import { AzureServiceClient } from "./azureServiceClient"; import { LongRunningOperationStates } from "./util/constants"; export type LROPollStrategyType = "AzureAsyncOperation" | "Location" | "GetResource"; export interface LROPollState { pollStrategyType: LROPollStrategyType; initialResponse: HttpOperationResponse; state: LongRunningOperationStates; mostRecentRequest: WebResource; mostRecentResponse: HttpOperationResponse; resource: any; azureAsyncOperationHeaderValue?: string; locationHeaderValue?: string; options?: RequestOptionsBase; } /** * A long-running operation polling strategy base class that other polling strategies should extend. */ export abstract class LROPollStrategy { constructor(private readonly _azureServiceClient: AzureServiceClient, protected readonly _pollState: LROPollState) { } public getOperationStatus(): LongRunningOperationStates { return this._pollState.state; } /** * Get whether or not this poll strategy's LRO is finished. * @returns Whether or not this poll strategy's LRO is finished. */ public isFinished(): boolean { return isFinished(this._pollState.state); } /** * Send poll requests that check the LRO's status until it is determined that the LRO is finished. * @returns Whether or not the LRO succeeded. */ public async pollUntilFinished(): Promise<boolean> { while (!this.isFinished()) { const delayInSeconds: number = getDelayInSeconds(this._azureServiceClient, this._pollState.mostRecentResponse); await delay(delayInSeconds * 1000); await this.sendPollRequest(); } return this.isFinalStatusAcceptable(); } /** * Send a single poll request that checks the LRO's status and return the response. If the LRO is * finished, then no request will be sent and the last response received will be returned. */ public abstract sendPollRequest(): Promise<void>; public abstract isFinalStatusAcceptable(): boolean; protected shouldDoFinalGetResourceRequest(): boolean { const initialRequestMethod: HttpMethods = this._pollState.initialResponse.request.method; return !this._pollState.resource && (initialRequestMethod === "PUT" || initialRequestMethod === "PATCH" || initialRequestMethod === "POST"); } protected abstract doFinalGetResourceRequest(): Promise<void>; public getMostRecentResponse(): HttpOperationResponse { return this._pollState.mostRecentResponse; } public async getOperationResponse(): Promise<HttpOperationResponse> { if (this.shouldDoFinalGetResourceRequest()) { await this.doFinalGetResourceRequest(); } const response: HttpOperationResponse = this._pollState.mostRecentResponse; const result: HttpOperationResponse = { ...response, headers: response.headers.clone() }; const resource: any = this._pollState.resource; if (!resource) { result.bodyAsText = response.bodyAsText; result.parsedBody = response.parsedBody; } else if (typeof resource.valueOf() === "string") { result.bodyAsText = resource; try { result.parsedBody = JSON.parse(resource); } catch (err) { // There was an error parsing the JSON. Hence we set the resource as-is. Most likely, the // resource is a string that was already parsed. result.parsedBody = resource; } } else { result.bodyAsText = JSON.stringify(resource); result.parsedBody = resource; } return result; } public getRestError(): RestError { const error = new RestError(""); error.request = stripRequest(this._pollState.mostRecentRequest); error.response = this._pollState.mostRecentResponse; error.message = `Long running operation failed with status: "${this._pollState.state}".`; error.body = this._pollState.resource; if (error.body) { const innerError: any = error.body.error; if (innerError) { if (innerError.message) { error.message = `Long running operation failed with error: "${innerError.message}".`; } if (innerError.code) { error.code = innerError.code; } } } return error; } protected updateState(url: string, shouldDeserialize: boolean | ((response: HttpOperationResponse) => boolean)): Promise<void> { return this.updateOperationStatus(url, shouldDeserialize).then(result => { this._pollState.state = getProvisioningState(result.parsedBody) || "Succeeded"; this._pollState.mostRecentResponse = result; this._pollState.mostRecentRequest = result.request; this._pollState.resource = getResponseBody(result); }).catch((error) => { let resultStatus: number | undefined; if (error.response && error.response.status) { resultStatus = error.response.status; if (this._pollState.initialResponse.request.method !== "DELETE" || resultStatus! < 400 || 499 < resultStatus!) { throw error; } } else { throw error; } }); } /** * Retrieves operation status by querying the operation URL. * @param {string} statusUrl URL used to poll operation result. */ protected updateOperationStatus(statusUrl: string, shouldDeserialize: boolean | ((response: HttpOperationResponse) => boolean)): Promise<HttpOperationResponse> { const requestUrl: string = statusUrl.replace(" ", "%20"); const httpRequest = new WebResource(requestUrl, "GET"); const pollState: LROPollState = this._pollState; httpRequest.operationSpec = pollState.mostRecentRequest.operationSpec; httpRequest.shouldDeserialize = shouldDeserialize; httpRequest.operationResponseGetter = getOperationResponse; const options: RequestOptionsBase | undefined = pollState.options; if (options && options.customHeaders) { const customHeaders = options.customHeaders; for (const headerName of Object.keys(customHeaders)) { httpRequest.headers.set(headerName, customHeaders[headerName]); } } return this._azureServiceClient.sendRequest(httpRequest); } public getPollState(): LROPollState { return this._pollState; } } function getOperationResponse(operationSpec: OperationSpec, response: HttpOperationResponse): OperationResponse | undefined { const statusCode: number = response.status; const operationResponses: { [statusCode: string]: OperationResponse } = operationSpec.responses; let result: OperationResponse | undefined = operationResponses[statusCode]; if (!result) { if (statusCode === 200) { result = operationResponses[201] || operationResponses[202]; } else if (201 <= statusCode && statusCode <= 299) { result = {}; } } return result; } export function getDelayInSeconds(azureServiceClient: AzureServiceClient, previousResponse: HttpOperationResponse): number { let delayInSeconds = 30; if (azureServiceClient.longRunningOperationRetryTimeout != undefined) { delayInSeconds = azureServiceClient.longRunningOperationRetryTimeout; } else { const retryAfterHeaderValue: string | undefined = previousResponse.headers.get("retry-after"); if (retryAfterHeaderValue) { const retryAfterDelayInSeconds: number = parseInt(retryAfterHeaderValue); if (!Number.isNaN(retryAfterDelayInSeconds)) { delayInSeconds = retryAfterDelayInSeconds; } } } return delayInSeconds; } function getProvisioningState(responseBody: any): LongRunningOperationStates | undefined { let result: LongRunningOperationStates | undefined; if (responseBody) { if (responseBody.provisioningState) { result = responseBody.provisioningState; } else if (responseBody.properties) { result = responseBody.properties.provisioningState; } } return result; } function getResponseBody(response: HttpOperationResponse): any { let result: any; try { if (response.parsedBody) { result = response.parsedBody; } else if (response.bodyAsText && response.bodyAsText.length > 0) { result = JSON.parse(response.bodyAsText); } } catch (error) { const deserializationError = new RestError(`Error "${error}" occurred in parsing the responseBody " + "while creating the PollingState for Long Running Operation- "${response.bodyAsText}"`); deserializationError.request = response.request; deserializationError.response = response; throw deserializationError; } return result; } function getStatusFromResponse(response: HttpOperationResponse, responseBody?: any): LongRunningOperationStates { if (responseBody == undefined) { responseBody = getResponseBody(response); } let result: LongRunningOperationStates; switch (response.status) { case 202: result = "InProgress"; break; case 204: result = "Succeeded"; break; case 201: result = getProvisioningState(responseBody) || "InProgress"; break; case 200: const provisioningState: LongRunningOperationStates | undefined = getProvisioningState(responseBody); if (provisioningState) { result = provisioningState; } else if (getAzureAsyncOperationHeaderValue(response) || getLocationHeaderValue(response)) { result = "InProgress"; } else { result = "Succeeded"; } break; default: result = "Failed"; break; } return result; } const terminalStates: LongRunningOperationStates[] = ["Succeeded", "Failed", "Canceled", "Cancelled"]; /** * Get whether or not a long-running operation with the provided status is finished. * @param status The current status of a long-running operation. * @returns Whether or not a long-running operation with the provided status is finished. */ export function isFinished(status: LongRunningOperationStates): boolean { let result = false; for (const terminalState of terminalStates) { if (longRunningOperationStatesEqual(status, terminalState)) { result = true; break; } } return result; } export function longRunningOperationStatesEqual(lhs: LongRunningOperationStates, rhs: LongRunningOperationStates): boolean { const lhsLowerCased: string = lhs && lhs.toLowerCase(); const rhsLowerCased: string = rhs && rhs.toLowerCase(); return lhsLowerCased === rhsLowerCased; } /** * Create a new long-running operation polling strategy based on the provided initial response. * @param initialResponse The initial response to the long-running operation's initial request. * @param azureServiceClient The AzureServiceClient that was used to send the initial request. * @param options Any options that were provided to the initial request. */ export function createLROPollStrategyFromInitialResponse(initialResponse: HttpOperationResponse, azureServiceClient: AzureServiceClient, options?: RequestOptionsBase): LROPollStrategy | undefined { const initialRequestMethod: HttpMethods = initialResponse.request.method; const initialResponseStatus: number = initialResponse.status; let lroPollStrategyType: LROPollStrategyType | undefined; if (getAzureAsyncOperationHeaderValue(initialResponse)) { lroPollStrategyType = "AzureAsyncOperation"; } else if (getLocationHeaderValue(initialResponse)) { lroPollStrategyType = "Location"; } else if (initialRequestMethod === "PUT" || initialRequestMethod === "PATCH") { lroPollStrategyType = "GetResource"; } else if (initialResponseStatus !== 201 && initialResponseStatus !== 202 && !isFinished(getStatusFromResponse(initialResponse))) { throw new Error("Can't determine long running operation polling strategy."); } let result: LROPollStrategy | undefined; if (lroPollStrategyType) { const resource: any = getResponseBody(initialResponse); const lroPollState: LROPollState = { pollStrategyType: lroPollStrategyType, options: options, initialResponse: initialResponse, mostRecentResponse: initialResponse, mostRecentRequest: initialResponse.request, azureAsyncOperationHeaderValue: getAzureAsyncOperationHeaderValue(initialResponse), locationHeaderValue: getLocationHeaderValue(initialResponse), resource: resource, state: getStatusFromResponse(initialResponse, resource) }; result = createLROPollStrategyFromPollState(azureServiceClient, lroPollState); } else { result = undefined; } return result; } export function createLROPollStrategyFromPollState(azureServiceClient: AzureServiceClient, lroPollState: LROPollState): LROPollStrategy | undefined { let result: LROPollStrategy; switch (lroPollState.pollStrategyType) { case "AzureAsyncOperation": result = new AzureAsyncOperationLROPollStrategy(azureServiceClient, lroPollState); break; case "Location": result = new LocationLROPollStrategy(azureServiceClient, lroPollState); break; case "GetResource": result = new GetResourceLROPollStrategy(azureServiceClient, lroPollState); break; default: throw new Error(`Unrecognized LRO poll strategy type: "${lroPollState.pollStrategyType}"`); break; } return result; } function getLocationHeaderValue(response: HttpOperationResponse): string | undefined { return response.headers.get("location"); } /** * A long-running operation polling strategy that is based on the location header. */ class LocationLROPollStrategy extends LROPollStrategy { private locationStrategyShouldDeserialize(parsedResponse: HttpOperationResponse): boolean { let shouldDeserialize = false; const initialResponse: HttpOperationResponse = this._pollState.initialResponse; const initialRequestMethod: HttpMethods = initialResponse.request.method; const statusCode: number = parsedResponse.status; if (statusCode === 200 || (statusCode === 201 && (initialRequestMethod === "PUT" || initialRequestMethod === "PATCH")) || (statusCode === 204 && (initialRequestMethod === "DELETE" || initialRequestMethod === "POST"))) { shouldDeserialize = true; } return shouldDeserialize; } /** * Retrieve PUT operation status by polling from "location" header. * @param {string} method - The HTTP method. * @param {PollingState} pollingState - The object to persist current operation state. */ public sendPollRequest(): Promise<void> { const lroPollState: LROPollState = this._pollState; return this.updateOperationStatus(lroPollState.locationHeaderValue!, this.locationStrategyShouldDeserialize.bind(this)).then((result: HttpOperationResponse) => { const locationHeaderValue: string | undefined = getLocationHeaderValue(result); if (locationHeaderValue) { lroPollState.locationHeaderValue = locationHeaderValue; } lroPollState.mostRecentResponse = result; lroPollState.mostRecentRequest = result.request; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; const initialRequestMethod: HttpMethods = initialResponse.request.method; const initialResponseStatusCode: number = initialResponse.status; const statusCode: number = result.status; if (statusCode === 202) { lroPollState.state = "InProgress"; } else if (statusCode === 200 || (statusCode === 201 && (initialRequestMethod === "PUT" || initialRequestMethod === "PATCH")) || (statusCode === 204 && (initialRequestMethod === "DELETE" || initialRequestMethod === "POST"))) { lroPollState.state = "Succeeded"; lroPollState.resource = getResponseBody(result); } else if (statusCode === 404 && initialRequestMethod === "POST" && (initialResponseStatusCode === 200 || initialResponseStatusCode === 201 || initialResponseStatusCode === 202)) { lroPollState.state = "Failed"; lroPollState.resource = getResponseBody(result); } else if (400 <= statusCode && statusCode <= 499) { const resultBody: string = result.bodyAsText!; let errorMessage: string = resultBody; try { const resultObject = JSON.parse(resultBody); errorMessage = resultObject.message; } catch (parseError) { // Ignore the exception, use resultBody as the error message } throw new RestError(errorMessage, undefined, statusCode, stripRequest(result.request), result, resultBody); } else { throw new Error(`The response with status code ${statusCode} from polling for long running operation url "${lroPollState.locationHeaderValue}" is not valid.`); } }); } public isFinalStatusAcceptable(): boolean { const lroPollState: LROPollState = this._pollState; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; const initialResponseStatusCode: number = initialResponse.status; return longRunningOperationStatesEqual(lroPollState.state, "Succeeded") || (initialResponse.request.method === "POST" && lroPollState.mostRecentResponse.status === 404 && (initialResponseStatusCode === 200 || initialResponseStatusCode === 201 || initialResponseStatusCode === 202)); } protected shouldDoFinalGetResourceRequest(): boolean { const lroPollState: LROPollState = this._pollState; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; let result: boolean; const initialRequestMethod: HttpMethods = initialResponse.request.method; const initialResponseStatusCode: number = initialResponse.status; if (initialRequestMethod === "POST" && lroPollState.mostRecentResponse.status === 404 && (initialResponseStatusCode === 200 || initialResponseStatusCode === 201 || initialResponseStatusCode === 202)) { result = false; } else { result = super.shouldDoFinalGetResourceRequest() || (initialRequestMethod === "POST" && initialResponseStatusCode === 201); } return result; } protected doFinalGetResourceRequest(): Promise<void> { const lroPollState: LROPollState = this._pollState; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; let getResourceRequestUrl: string; const initialResponseStatusCode: number = initialResponse.status; const initialRequest: WebResource = initialResponse.request; if (initialRequest.method === "POST" && (initialResponseStatusCode === 200 || initialResponseStatusCode === 201 || initialResponseStatusCode === 202)) { getResourceRequestUrl = lroPollState.locationHeaderValue!; } else { getResourceRequestUrl = initialRequest.url; } return this.updateState(getResourceRequestUrl, true); } } function getAzureAsyncOperationHeaderValue(response: HttpOperationResponse): string | undefined { return response.headers.get("azure-asyncoperation"); } /** * A long-running operation polling strategy that is based on the azure-asyncoperation header. */ class AzureAsyncOperationLROPollStrategy extends LROPollStrategy { /** * Retrieve operation status by polling from "azure-asyncoperation" header. * @param {PollingState} pollingState - The object to persist current operation state. * @param {boolean} inPostOrDelete - Invoked by Post Or Delete operation. */ public sendPollRequest(): Promise<void> { const lroPollState: LROPollState = this._pollState; return this.updateOperationStatus(lroPollState.azureAsyncOperationHeaderValue!, false).then((response: HttpOperationResponse) => { const statusCode: number = response.status; const parsedResponse: any = response.parsedBody; if (statusCode !== 200 && statusCode !== 201 && statusCode !== 202 && statusCode !== 204) { const error = new RestError(`Invalid status code (${statusCode}) with response body "${response.bodyAsText}" occurred when polling for operation status.`); error.statusCode = statusCode; error.request = stripRequest(response.request); error.response = response; error.body = parsedResponse; throw error; } if (!parsedResponse) { throw new Error("The response from long running operation does not contain a body."); } else if (!parsedResponse.status) { throw new Error(`The response "${response.bodyAsText}" from long running operation does not contain the status property.`); } const azureAsyncOperationHeaderValue: string | undefined = getAzureAsyncOperationHeaderValue(response); if (azureAsyncOperationHeaderValue) { lroPollState.azureAsyncOperationHeaderValue = azureAsyncOperationHeaderValue; } lroPollState.state = parsedResponse.status; lroPollState.mostRecentResponse = response; lroPollState.mostRecentRequest = response.request; lroPollState.resource = getResponseBody(response); }); } protected shouldDoFinalGetResourceRequest(): boolean { const lroPollState: LROPollState = this._pollState; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; const initialRequestMethod: HttpMethods = initialResponse.request.method; let result = false; if (initialRequestMethod === "PUT" || initialRequestMethod === "PATCH") { result = true; } else { if (lroPollState.locationHeaderValue) { const initialResponseStatusCode: number = initialResponse.status; if (initialRequestMethod === "POST") { result = initialResponseStatusCode === 200 || initialResponseStatusCode === 201; } else if (initialRequestMethod === "DELETE") { result = initialResponseStatusCode === 200 || initialResponseStatusCode === 202; } } } return result; } protected doFinalGetResourceRequest(): Promise<void> { const lroPollState: LROPollState = this._pollState; const locationHeaderValue: string | undefined = lroPollState.locationHeaderValue; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; const initialRequest: WebResource = initialResponse.request; let getResourceRequestUrl: string = initialRequest.url; if (locationHeaderValue) { const initialRequestMethod: HttpMethods = initialRequest.method; const initialResponseStatusCode: number = initialResponse.status; if (initialRequestMethod === "POST" && (initialResponseStatusCode === 200 || initialResponseStatusCode === 201 || initialResponseStatusCode === 202)) { getResourceRequestUrl = locationHeaderValue; } else if (initialRequestMethod === "DELETE" && (initialResponseStatusCode === 200 || initialResponseStatusCode === 202)) { getResourceRequestUrl = locationHeaderValue; } } return this.updateState(getResourceRequestUrl, true); } public isFinalStatusAcceptable(): boolean { const lroPollState: LROPollState = this._pollState; const initialResponse: HttpOperationResponse = lroPollState.initialResponse; const initialResponseStatusCode: number = initialResponse.status; return longRunningOperationStatesEqual(lroPollState.state, "Succeeded") || (initialResponse.request.method === "POST" && (initialResponseStatusCode === 200 || initialResponseStatusCode === 201)); } } /** * A long-running operation polling strategy that is based on the resource's provisioning state. */ class GetResourceLROPollStrategy extends LROPollStrategy { public sendPollRequest(): Promise<void> { const lroPollState: LROPollState = this._pollState; return this.updateOperationStatus(lroPollState.initialResponse.request.url, false).then(result => { const statusCode: number = result.status; const responseBody: any = result.parsedBody; if (statusCode !== 200 && statusCode !== 201 && statusCode !== 202 && statusCode !== 204) { const error = new RestError(`Invalid status code with response body "${result.bodyAsText}" occurred when polling for operation status.`); error.statusCode = statusCode; error.request = stripRequest(result.request); error.response = result; error.body = responseBody; throw error; } if (!result.parsedBody) { throw new Error("The response from long running operation does not contain a body."); } lroPollState.state = getProvisioningState(result.parsedBody) || "Succeeded"; lroPollState.mostRecentResponse = result; lroPollState.mostRecentRequest = result.request; lroPollState.resource = getResponseBody(result); }); } public isFinalStatusAcceptable(): boolean { return longRunningOperationStatesEqual(this._pollState.state, "Succeeded"); } protected doFinalGetResourceRequest(): Promise<void> { return this.sendPollRequest(); } }