feat: add @actions/cache

This commit is contained in:
xHyroM 2022-07-12 09:00:22 +02:00
parent b15fb7d098
commit 16e8c96a41
1932 changed files with 261172 additions and 10 deletions

View file

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { AgentSettings } from "../serviceClient";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
import { HttpOperationResponse } from "../httpOperationResponse";
import { WebResourceLike } from "../webResource";
const agentNotSupportedInBrowser = new Error("AgentPolicy is not supported in browser environment");
export function agentPolicy(_agentSettings?: AgentSettings): RequestPolicyFactory {
return {
create: (_nextPolicy: RequestPolicy, _options: RequestPolicyOptionsLike) => {
throw agentNotSupportedInBrowser;
},
};
}
export class AgentPolicy extends BaseRequestPolicy {
constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) {
super(nextPolicy, options);
throw agentNotSupportedInBrowser;
}
public sendRequest(_request: WebResourceLike): Promise<HttpOperationResponse> {
throw agentNotSupportedInBrowser;
}
}

View file

@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { AgentSettings } from "../serviceClient";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
import { HttpOperationResponse } from "../httpOperationResponse";
import { WebResourceLike } from "../webResource";
export function agentPolicy(agentSettings?: AgentSettings): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new AgentPolicy(nextPolicy, options, agentSettings!);
},
};
}
export class AgentPolicy extends BaseRequestPolicy {
agentSettings: AgentSettings;
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
agentSettings: AgentSettings
) {
super(nextPolicy, options);
this.agentSettings = agentSettings;
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
if (!request.agentSettings) {
request.agentSettings = this.agentSettings;
}
return this._nextPolicy.sendRequest(request);
}
}

View file

@ -0,0 +1,294 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import { OperationResponse } from "../operationResponse";
import { OperationSpec, isStreamOperation } from "../operationSpec";
import { RestError } from "../restError";
import { Mapper, MapperType } from "../serializer";
import * as utils from "../util/utils";
import { parseXML } from "../util/xml";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
/**
* The content-types that will indicate that an operation response should be deserialized in a
* particular way.
*/
export interface DeserializationContentTypes {
/**
* The content-types that indicate that an operation response should be deserialized as JSON.
* Defaults to [ "application/json", "text/json" ].
*/
json?: string[];
/**
* The content-types that indicate that an operation response should be deserialized as XML.
* Defaults to [ "application/xml", "application/atom+xml" ].
*/
xml?: string[];
}
/**
* Create a new serialization RequestPolicyCreator that will serialized HTTP request bodies as they
* pass through the HTTP pipeline.
*/
export function deserializationPolicy(
deserializationContentTypes?: DeserializationContentTypes
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new DeserializationPolicy(nextPolicy, deserializationContentTypes, options);
},
};
}
export const defaultJsonContentTypes = ["application/json", "text/json"];
export const defaultXmlContentTypes = ["application/xml", "application/atom+xml"];
/**
* A RequestPolicy that will deserialize HTTP response bodies and headers as they pass through the
* HTTP pipeline.
*/
export class DeserializationPolicy extends BaseRequestPolicy {
public readonly jsonContentTypes: string[];
public readonly xmlContentTypes: string[];
constructor(
nextPolicy: RequestPolicy,
deserializationContentTypes: DeserializationContentTypes | undefined,
options: RequestPolicyOptionsLike
) {
super(nextPolicy, options);
this.jsonContentTypes =
(deserializationContentTypes && deserializationContentTypes.json) || defaultJsonContentTypes;
this.xmlContentTypes =
(deserializationContentTypes && deserializationContentTypes.xml) || defaultXmlContentTypes;
}
public async sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy
.sendRequest(request)
.then((response: HttpOperationResponse) =>
deserializeResponseBody(this.jsonContentTypes, this.xmlContentTypes, response)
);
}
}
function getOperationResponse(
parsedResponse: HttpOperationResponse
): undefined | OperationResponse {
let result: OperationResponse | undefined;
const request: WebResourceLike = parsedResponse.request;
const operationSpec: OperationSpec | undefined = request.operationSpec;
if (operationSpec) {
const operationResponseGetter:
| undefined
| ((
operationSpec: OperationSpec,
response: HttpOperationResponse
) => undefined | OperationResponse) = request.operationResponseGetter;
if (!operationResponseGetter) {
result = operationSpec.responses[parsedResponse.status];
} else {
result = operationResponseGetter(operationSpec, parsedResponse);
}
}
return result;
}
function shouldDeserializeResponse(parsedResponse: HttpOperationResponse): boolean {
const shouldDeserialize: undefined | boolean | ((response: HttpOperationResponse) => boolean) =
parsedResponse.request.shouldDeserialize;
let result: boolean;
if (shouldDeserialize === undefined) {
result = true;
} else if (typeof shouldDeserialize === "boolean") {
result = shouldDeserialize;
} else {
result = shouldDeserialize(parsedResponse);
}
return result;
}
export function deserializeResponseBody(
jsonContentTypes: string[],
xmlContentTypes: string[],
response: HttpOperationResponse
): Promise<HttpOperationResponse> {
return parse(jsonContentTypes, xmlContentTypes, response).then((parsedResponse) => {
const shouldDeserialize: boolean = shouldDeserializeResponse(parsedResponse);
if (shouldDeserialize) {
const operationSpec: OperationSpec | undefined = parsedResponse.request.operationSpec;
if (operationSpec && operationSpec.responses) {
const statusCode: number = parsedResponse.status;
const expectedStatusCodes: string[] = Object.keys(operationSpec.responses);
const hasNoExpectedStatusCodes: boolean =
expectedStatusCodes.length === 0 ||
(expectedStatusCodes.length === 1 && expectedStatusCodes[0] === "default");
const responseSpec: OperationResponse | undefined = getOperationResponse(parsedResponse);
const isExpectedStatusCode: boolean = hasNoExpectedStatusCodes
? 200 <= statusCode && statusCode < 300
: !!responseSpec;
if (!isExpectedStatusCode) {
const defaultResponseSpec: OperationResponse = operationSpec.responses.default;
if (defaultResponseSpec) {
const initialErrorMessage: string = isStreamOperation(operationSpec)
? `Unexpected status code: ${statusCode}`
: (parsedResponse.bodyAsText as string);
const error = new RestError(initialErrorMessage);
error.statusCode = statusCode;
error.request = utils.stripRequest(parsedResponse.request);
error.response = utils.stripResponse(parsedResponse);
let parsedErrorResponse: { [key: string]: any } = parsedResponse.parsedBody;
try {
if (parsedErrorResponse) {
const defaultResponseBodyMapper: Mapper | undefined =
defaultResponseSpec.bodyMapper;
if (
defaultResponseBodyMapper &&
defaultResponseBodyMapper.serializedName === "CloudError"
) {
if (parsedErrorResponse.error) {
parsedErrorResponse = parsedErrorResponse.error;
}
if (parsedErrorResponse.code) {
error.code = parsedErrorResponse.code;
}
if (parsedErrorResponse.message) {
error.message = parsedErrorResponse.message;
}
} else {
let internalError: any = parsedErrorResponse;
if (parsedErrorResponse.error) {
internalError = parsedErrorResponse.error;
}
error.code = internalError.code;
if (internalError.message) {
error.message = internalError.message;
}
}
if (defaultResponseBodyMapper) {
let valueToDeserialize: any = parsedErrorResponse;
if (
operationSpec.isXML &&
defaultResponseBodyMapper.type.name === MapperType.Sequence
) {
valueToDeserialize =
typeof parsedErrorResponse === "object"
? parsedErrorResponse[defaultResponseBodyMapper.xmlElementName!]
: [];
}
error.body = operationSpec.serializer.deserialize(
defaultResponseBodyMapper,
valueToDeserialize,
"error.body"
);
}
}
} catch (defaultError) {
error.message = `Error \"${defaultError.message}\" occurred in deserializing the responseBody - \"${parsedResponse.bodyAsText}\" for the default response.`;
}
return Promise.reject(error);
}
} else if (responseSpec) {
if (responseSpec.bodyMapper) {
let valueToDeserialize: any = parsedResponse.parsedBody;
if (operationSpec.isXML && responseSpec.bodyMapper.type.name === MapperType.Sequence) {
valueToDeserialize =
typeof valueToDeserialize === "object"
? valueToDeserialize[responseSpec.bodyMapper.xmlElementName!]
: [];
}
try {
parsedResponse.parsedBody = operationSpec.serializer.deserialize(
responseSpec.bodyMapper,
valueToDeserialize,
"operationRes.parsedBody"
);
} catch (error) {
const restError = new RestError(
`Error ${error} occurred in deserializing the responseBody - ${parsedResponse.bodyAsText}`
);
restError.request = utils.stripRequest(parsedResponse.request);
restError.response = utils.stripResponse(parsedResponse);
return Promise.reject(restError);
}
} else if (operationSpec.httpMethod === "HEAD") {
// head methods never have a body, but we return a boolean to indicate presence/absence of the resource
parsedResponse.parsedBody = response.status >= 200 && response.status < 300;
}
if (responseSpec.headersMapper) {
parsedResponse.parsedHeaders = operationSpec.serializer.deserialize(
responseSpec.headersMapper,
parsedResponse.headers.rawHeaders(),
"operationRes.parsedHeaders"
);
}
}
}
}
return Promise.resolve(parsedResponse);
});
}
function parse(
jsonContentTypes: string[],
xmlContentTypes: string[],
operationResponse: HttpOperationResponse
): Promise<HttpOperationResponse> {
const errorHandler = (err: Error & { code: string }) => {
const msg = `Error "${err}" occurred while parsing the response body - ${operationResponse.bodyAsText}.`;
const errCode = err.code || RestError.PARSE_ERROR;
const e = new RestError(
msg,
errCode,
operationResponse.status,
operationResponse.request,
operationResponse,
operationResponse.bodyAsText
);
return Promise.reject(e);
};
if (!operationResponse.request.streamResponseBody && operationResponse.bodyAsText) {
const text = operationResponse.bodyAsText;
const contentType: string = operationResponse.headers.get("Content-Type") || "";
const contentComponents: string[] = !contentType
? []
: contentType.split(";").map((component) => component.toLowerCase());
if (
contentComponents.length === 0 ||
contentComponents.some((component) => jsonContentTypes.indexOf(component) !== -1)
) {
return new Promise<HttpOperationResponse>((resolve) => {
operationResponse.parsedBody = JSON.parse(text);
resolve(operationResponse);
}).catch(errorHandler);
} else if (contentComponents.some((component) => xmlContentTypes.indexOf(component) !== -1)) {
return parseXML(text)
.then((body) => {
operationResponse.parsedBody = body;
return operationResponse;
})
.catch(errorHandler);
}
}
return Promise.resolve(operationResponse);
}

View file

@ -0,0 +1,220 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
import { RestError } from "../restError";
export interface RetryData {
retryCount: number;
retryInterval: number;
error?: RetryError;
}
export interface RetryError extends Error {
message: string;
code?: string;
innerError?: RetryError;
}
export function exponentialRetryPolicy(
retryCount?: number,
retryInterval?: number,
minRetryInterval?: number,
maxRetryInterval?: number
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new ExponentialRetryPolicy(
nextPolicy,
options,
retryCount,
retryInterval,
minRetryInterval,
maxRetryInterval
);
},
};
}
const DEFAULT_CLIENT_RETRY_INTERVAL = 1000 * 30;
const DEFAULT_CLIENT_RETRY_COUNT = 3;
const DEFAULT_CLIENT_MAX_RETRY_INTERVAL = 1000 * 90;
const DEFAULT_CLIENT_MIN_RETRY_INTERVAL = 1000 * 3;
/**
* @class
* Instantiates a new "ExponentialRetryPolicyFilter" instance.
*/
export class ExponentialRetryPolicy extends BaseRequestPolicy {
/**
* The client retry count.
*/
retryCount: number;
/**
* The client retry interval in milliseconds.
*/
retryInterval: number;
/**
* The minimum retry interval in milliseconds.
*/
minRetryInterval: number;
/**
* The maximum retry interval in milliseconds.
*/
maxRetryInterval: number;
/**
* @constructor
* @param {RequestPolicy} nextPolicy The next RequestPolicy in the pipeline chain.
* @param {RequestPolicyOptionsLike} options The options for this RequestPolicy.
* @param {number} [retryCount] The client retry count.
* @param {number} [retryInterval] The client retry interval, in milliseconds.
* @param {number} [minRetryInterval] The minimum retry interval, in milliseconds.
* @param {number} [maxRetryInterval] The maximum retry interval, in milliseconds.
*/
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
retryCount?: number,
retryInterval?: number,
minRetryInterval?: number,
maxRetryInterval?: number
) {
super(nextPolicy, options);
function isNumber(n: any): n is number {
return typeof n === "number";
}
this.retryCount = isNumber(retryCount) ? retryCount : DEFAULT_CLIENT_RETRY_COUNT;
this.retryInterval = isNumber(retryInterval) ? retryInterval : DEFAULT_CLIENT_RETRY_INTERVAL;
this.minRetryInterval = isNumber(minRetryInterval)
? minRetryInterval
: DEFAULT_CLIENT_MIN_RETRY_INTERVAL;
this.maxRetryInterval = isNumber(maxRetryInterval)
? maxRetryInterval
: DEFAULT_CLIENT_MAX_RETRY_INTERVAL;
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy
.sendRequest(request.clone())
.then((response) => retry(this, request, response))
.catch((error) => retry(this, request, error.response, undefined, error));
}
}
/**
* Determines if the operation should be retried and how long to wait until the next retry.
*
* @param {ExponentialRetryPolicy} policy The ExponentialRetryPolicy that this function is being called against.
* @param {number} statusCode The HTTP status code.
* @param {RetryData} retryData The retry data.
* @return {boolean} True if the operation qualifies for a retry; false otherwise.
*/
function shouldRetry(
policy: ExponentialRetryPolicy,
statusCode: number | undefined,
retryData: RetryData
): boolean {
if (
statusCode == undefined ||
(statusCode < 500 && statusCode !== 408) ||
statusCode === 501 ||
statusCode === 505
) {
return false;
}
let currentCount: number;
if (!retryData) {
throw new Error("retryData for the ExponentialRetryPolicyFilter cannot be null.");
} else {
currentCount = retryData && retryData.retryCount;
}
return currentCount < policy.retryCount;
}
/**
* Updates the retry data for the next attempt.
*
* @param {ExponentialRetryPolicy} policy The ExponentialRetryPolicy that this function is being called against.
* @param {RetryData} retryData The retry data.
* @param {RetryError} [err] The operation"s error, if any.
*/
function updateRetryData(
policy: ExponentialRetryPolicy,
retryData?: RetryData,
err?: RetryError
): RetryData {
if (!retryData) {
retryData = {
retryCount: 0,
retryInterval: 0,
};
}
if (err) {
if (retryData.error) {
err.innerError = retryData.error;
}
retryData.error = err;
}
// Adjust retry count
retryData.retryCount++;
// Adjust retry interval
let incrementDelta = Math.pow(2, retryData.retryCount) - 1;
const boundedRandDelta =
policy.retryInterval * 0.8 +
Math.floor(Math.random() * (policy.retryInterval * 1.2 - policy.retryInterval * 0.8));
incrementDelta *= boundedRandDelta;
retryData.retryInterval = Math.min(
policy.minRetryInterval + incrementDelta,
policy.maxRetryInterval
);
return retryData;
}
function retry(
policy: ExponentialRetryPolicy,
request: WebResourceLike,
response?: HttpOperationResponse,
retryData?: RetryData,
requestError?: RetryError
): Promise<HttpOperationResponse> {
retryData = updateRetryData(policy, retryData, requestError);
const isAborted: boolean | undefined = request.abortSignal && request.abortSignal.aborted;
if (!isAborted && shouldRetry(policy, response && response.status, retryData)) {
return utils
.delay(retryData.retryInterval)
.then(() => policy._nextPolicy.sendRequest(request.clone()))
.then((res) => retry(policy, request, res, retryData, undefined))
.catch((err) => retry(policy, request, response, retryData, err));
} else if (isAborted || requestError || !response) {
// If the operation failed in the end, return all errors instead of just the last one
const err =
retryData.error ||
new RestError(
"Failed to send the request.",
RestError.REQUEST_SEND_ERROR,
response && response.status,
response && response.request,
response
);
return Promise.reject(err);
} else {
return Promise.resolve(response);
}
}

View file

@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
export function generateClientRequestIdPolicy(
requestIdHeaderName = "x-ms-client-request-id"
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new GenerateClientRequestIdPolicy(nextPolicy, options, requestIdHeaderName);
},
};
}
export class GenerateClientRequestIdPolicy extends BaseRequestPolicy {
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
private _requestIdHeaderName: string
) {
super(nextPolicy, options);
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
if (!request.headers.contains(this._requestIdHeaderName)) {
request.headers.set(this._requestIdHeaderName, utils.generateUuid());
}
return this._nextPolicy.sendRequest(request);
}
}

View file

@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
export function logPolicy(logger: any = console.log): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new LogPolicy(nextPolicy, options, logger);
},
};
}
export class LogPolicy extends BaseRequestPolicy {
logger?: any;
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
logger: any = console.log
) {
super(nextPolicy, options);
this.logger = logger;
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy.sendRequest(request).then((response) => logResponse(this, response));
}
}
function logResponse(
policy: LogPolicy,
response: HttpOperationResponse
): Promise<HttpOperationResponse> {
policy.logger(`>> Request: ${JSON.stringify(response.request, undefined, 2)}`);
policy.logger(`>> Response status code: ${response.status}`);
const responseBody = response.bodyAsText;
policy.logger(`>> Body: ${responseBody}`);
return Promise.resolve(response);
}

View file

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
/*
* NOTE: When moving this file, please update "browser" section in package.json
* and "plugins" section in webpack.testconfig.ts.
*/
import { TelemetryInfo } from "./userAgentPolicy";
interface NavigatorEx extends Navigator {
// oscpu is not yet standards-compliant, but can not be undefined in TypeScript 3.6.2
readonly oscpu: string;
}
export function getDefaultUserAgentKey(): string {
return "x-ms-command-name";
}
export function getPlatformSpecificData(): TelemetryInfo[] {
const navigator = self.navigator as NavigatorEx;
const osInfo = {
key: "OS",
value: (navigator.oscpu || navigator.platform).replace(" ", ""),
};
return [osInfo];
}

View file

@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import * as os from "os";
import { TelemetryInfo } from "./userAgentPolicy";
import { Constants } from "../util/constants";
export function getDefaultUserAgentKey(): string {
return Constants.HeaderConstants.USER_AGENT;
}
export function getPlatformSpecificData(): TelemetryInfo[] {
const runtimeInfo = {
key: "Node",
value: process.version,
};
const osInfo = {
key: "OS",
value: `(${os.arch()}-${os.type()}-${os.release()})`,
};
return [runtimeInfo, osInfo];
}

View file

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { ProxySettings } from "../serviceClient";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
import { HttpOperationResponse } from "../httpOperationResponse";
import { WebResourceLike } from "../webResource";
const proxyNotSupportedInBrowser = new Error("ProxyPolicy is not supported in browser environment");
export function getDefaultProxySettings(_proxyUrl?: string): ProxySettings | undefined {
return undefined;
}
export function proxyPolicy(_proxySettings?: ProxySettings): RequestPolicyFactory {
return {
create: (_nextPolicy: RequestPolicy, _options: RequestPolicyOptionsLike) => {
throw proxyNotSupportedInBrowser;
},
};
}
export class ProxyPolicy extends BaseRequestPolicy {
constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) {
super(nextPolicy, options);
throw proxyNotSupportedInBrowser;
}
public sendRequest(_request: WebResourceLike): Promise<HttpOperationResponse> {
throw proxyNotSupportedInBrowser;
}
}

View file

@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
import { HttpOperationResponse } from "../httpOperationResponse";
import { ProxySettings } from "../serviceClient";
import { WebResourceLike } from "../webResource";
import { Constants } from "../util/constants";
import { URLBuilder } from "../url";
/**
* @internal
*/
export const noProxyList: string[] = loadNoProxy();
const byPassedList: Map<string, boolean> = new Map();
/**
* @internal
*/
export function getEnvironmentValue(name: string): string | undefined {
if (process.env[name]) {
return process.env[name];
} else if (process.env[name.toLowerCase()]) {
return process.env[name.toLowerCase()];
}
return undefined;
}
function loadEnvironmentProxyValue(): string | undefined {
if (!process) {
return undefined;
}
const httpsProxy = getEnvironmentValue(Constants.HTTPS_PROXY);
const allProxy = getEnvironmentValue(Constants.ALL_PROXY);
const httpProxy = getEnvironmentValue(Constants.HTTP_PROXY);
return httpsProxy || allProxy || httpProxy;
}
// Check whether the host of a given `uri` is in the noProxyList.
// If there's a match, any request sent to the same host won't have the proxy settings set.
// This implementation is a port of https://github.com/Azure/azure-sdk-for-net/blob/8cca811371159e527159c7eb65602477898683e2/sdk/core/Azure.Core/src/Pipeline/Internal/HttpEnvironmentProxy.cs#L210
function isBypassed(uri: string): boolean | undefined {
if (noProxyList.length === 0) {
return false;
}
const host = URLBuilder.parse(uri).getHost()!;
if (byPassedList.has(host)) {
return byPassedList.get(host);
}
let isBypassedFlag = false;
for (const pattern of noProxyList) {
if (pattern[0] === ".") {
// This should match either domain it self or any subdomain or host
// .foo.com will match foo.com it self or *.foo.com
if (host.endsWith(pattern)) {
isBypassedFlag = true;
} else {
if (host.length === pattern.length - 1 && host === pattern.slice(1)) {
isBypassedFlag = true;
}
}
} else {
if (host === pattern) {
isBypassedFlag = true;
}
}
}
byPassedList.set(host, isBypassedFlag);
return isBypassedFlag;
}
/**
* @internal
*/
export function loadNoProxy(): string[] {
const noProxy = getEnvironmentValue(Constants.NO_PROXY);
if (noProxy) {
return noProxy
.split(",")
.map((item) => item.trim())
.filter((item) => item.length);
}
return [];
}
/**
* @internal
*/
function extractAuthFromUrl(
url: string
): { username?: string; password?: string; urlWithoutAuth: string } {
const atIndex = url.indexOf("@");
if (atIndex === -1) {
return { urlWithoutAuth: url };
}
const schemeIndex = url.indexOf("://");
const authStart = schemeIndex !== -1 ? schemeIndex + 3 : 0;
const auth = url.substring(authStart, atIndex);
const colonIndex = auth.indexOf(":");
const hasPassword = colonIndex !== -1;
const username = hasPassword ? auth.substring(0, colonIndex) : auth;
const password = hasPassword ? auth.substring(colonIndex + 1) : undefined;
const urlWithoutAuth = url.substring(0, authStart) + url.substring(atIndex + 1);
return {
username,
password,
urlWithoutAuth,
};
}
export function getDefaultProxySettings(proxyUrl?: string): ProxySettings | undefined {
if (!proxyUrl) {
proxyUrl = loadEnvironmentProxyValue();
if (!proxyUrl) {
return undefined;
}
}
const { username, password, urlWithoutAuth } = extractAuthFromUrl(proxyUrl);
const parsedUrl = URLBuilder.parse(urlWithoutAuth);
const schema = parsedUrl.getScheme() ? parsedUrl.getScheme() + "://" : "";
return {
host: schema + parsedUrl.getHost(),
port: Number.parseInt(parsedUrl.getPort() || "80"),
username,
password,
};
}
export function proxyPolicy(proxySettings?: ProxySettings): RequestPolicyFactory {
if (!proxySettings) {
proxySettings = getDefaultProxySettings();
}
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new ProxyPolicy(nextPolicy, options, proxySettings!);
},
};
}
export class ProxyPolicy extends BaseRequestPolicy {
proxySettings: ProxySettings;
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
proxySettings: ProxySettings
) {
super(nextPolicy, options);
this.proxySettings = proxySettings;
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
if (!request.proxySettings && !isBypassed(request.url)) {
request.proxySettings = this.proxySettings;
}
return this._nextPolicy.sendRequest(request);
}
}

View file

@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import { URLBuilder } from "../url";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
/**
* Options for how redirect responses are handled.
*/
export interface RedirectOptions {
/*
* When true, redirect responses are followed. Defaults to true.
*/
handleRedirects: boolean;
/*
* The maximum number of times the redirect URL will be tried before
* failing. Defaults to 20.
*/
maxRetries?: number;
}
export const DefaultRedirectOptions: RedirectOptions = {
handleRedirects: true,
maxRetries: 20,
};
export function redirectPolicy(maximumRetries = 20): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new RedirectPolicy(nextPolicy, options, maximumRetries);
},
};
}
export class RedirectPolicy extends BaseRequestPolicy {
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
readonly maxRetries = 20
) {
super(nextPolicy, options);
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy
.sendRequest(request)
.then((response) => handleRedirect(this, response, 0));
}
}
function handleRedirect(
policy: RedirectPolicy,
response: HttpOperationResponse,
currentRetries: number
): Promise<HttpOperationResponse> {
const { request, status } = response;
const locationHeader = response.headers.get("location");
if (
locationHeader &&
(status === 300 ||
(status === 301 && ["GET", "HEAD"].includes(request.method)) ||
(status === 302 && ["GET", "POST", "HEAD"].includes(request.method)) ||
(status === 303 && "POST" === request.method) ||
status === 307) &&
((request.redirectLimit !== undefined && currentRetries < request.redirectLimit) ||
(request.redirectLimit === undefined && currentRetries < policy.maxRetries))
) {
const builder = URLBuilder.parse(request.url);
builder.setPath(locationHeader);
request.url = builder.toString();
// POST request with Status code 302 and 303 should be converted into a
// redirected GET request if the redirect url is present in the location header
// reference: https://tools.ietf.org/html/rfc7231#page-57 && https://fetch.spec.whatwg.org/#http-redirect-fetch
if ((status === 302 || status === 303) && request.method === "POST") {
request.method = "GET";
delete request.body;
}
return policy._nextPolicy
.sendRequest(request)
.then((res) => handleRedirect(policy, res, currentRetries + 1))
.then((res) => recordRedirect(res, request.url));
}
return Promise.resolve(response);
}
function recordRedirect(response: HttpOperationResponse, redirect: string): HttpOperationResponse {
// This is called as the recursive calls to handleRedirect() unwind,
// only record the deepest/last redirect
if (!response.redirected) {
response.redirected = true;
response.url = redirect;
}
return response;
}

View file

@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import { HttpPipelineLogger } from "../httpPipelineLogger";
import { HttpPipelineLogLevel } from "../httpPipelineLogLevel";
import { WebResourceLike } from "../webResource";
/**
* Creates a new RequestPolicy per-request that uses the provided nextPolicy.
*/
export type RequestPolicyFactory = {
create(nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike): RequestPolicy;
};
export interface RequestPolicy {
sendRequest(httpRequest: WebResourceLike): Promise<HttpOperationResponse>;
}
export abstract class BaseRequestPolicy implements RequestPolicy {
protected constructor(
readonly _nextPolicy: RequestPolicy,
readonly _options: RequestPolicyOptionsLike
) {}
public abstract sendRequest(webResource: WebResourceLike): Promise<HttpOperationResponse>;
/**
* Get whether or not a log with the provided log level should be logged.
* @param logLevel The log level of the log that will be logged.
* @returns Whether or not a log with the provided log level should be logged.
*/
public shouldLog(logLevel: HttpPipelineLogLevel): boolean {
return this._options.shouldLog(logLevel);
}
/**
* Attempt to log the provided message to the provided logger. If no logger was provided or if
* the log level does not meat the logger's threshold, then nothing will be logged.
* @param logLevel The log level of this log.
* @param message The message of this log.
*/
public log(logLevel: HttpPipelineLogLevel, message: string): void {
this._options.log(logLevel, message);
}
}
/**
* Optional properties that can be used when creating a RequestPolicy.
*/
export interface RequestPolicyOptionsLike {
/**
* Get whether or not a log with the provided log level should be logged.
* @param logLevel The log level of the log that will be logged.
* @returns Whether or not a log with the provided log level should be logged.
*/
shouldLog(logLevel: HttpPipelineLogLevel): boolean;
/**
* Attempt to log the provided message to the provided logger. If no logger was provided or if
* the log level does not meet the logger's threshold, then nothing will be logged.
* @param logLevel The log level of this log.
* @param message The message of this log.
*/
log(logLevel: HttpPipelineLogLevel, message: string): void;
}
/**
* Optional properties that can be used when creating a RequestPolicy.
*/
export class RequestPolicyOptions implements RequestPolicyOptionsLike {
constructor(private _logger?: HttpPipelineLogger) {}
/**
* Get whether or not a log with the provided log level should be logged.
* @param logLevel The log level of the log that will be logged.
* @returns Whether or not a log with the provided log level should be logged.
*/
public shouldLog(logLevel: HttpPipelineLogLevel): boolean {
return (
!!this._logger &&
logLevel !== HttpPipelineLogLevel.OFF &&
logLevel <= this._logger.minimumLogLevel
);
}
/**
* Attempt to log the provided message to the provided logger. If no logger was provided or if
* the log level does not meat the logger's threshold, then nothing will be logged.
* @param logLevel The log level of this log.
* @param message The message of this log.
*/
public log(logLevel: HttpPipelineLogLevel, message: string): void {
if (this._logger && this.shouldLog(logLevel)) {
this._logger.log(logLevel, message);
}
}
}

View file

@ -0,0 +1,197 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
export function rpRegistrationPolicy(retryTimeout = 30): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new RPRegistrationPolicy(nextPolicy, options, retryTimeout);
},
};
}
export class RPRegistrationPolicy extends BaseRequestPolicy {
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
readonly _retryTimeout = 30
) {
super(nextPolicy, options);
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy
.sendRequest(request.clone())
.then((response) => registerIfNeeded(this, request, response));
}
}
function registerIfNeeded(
policy: RPRegistrationPolicy,
request: WebResourceLike,
response: HttpOperationResponse
): Promise<HttpOperationResponse> {
if (response.status === 409) {
const rpName = checkRPNotRegisteredError(response.bodyAsText as string);
if (rpName) {
const urlPrefix = extractSubscriptionUrl(request.url);
return (
registerRP(policy, urlPrefix, rpName, request)
// Autoregistration of ${provider} failed for some reason. We will not return this error
// instead will return the initial response with 409 status code back to the user.
// do nothing here as we are returning the original response at the end of this method.
.catch(() => false)
.then((registrationStatus) => {
if (registrationStatus) {
// Retry the original request. We have to change the x-ms-client-request-id
// otherwise Azure endpoint will return the initial 409 (cached) response.
request.headers.set("x-ms-client-request-id", utils.generateUuid());
return policy._nextPolicy.sendRequest(request.clone());
}
return response;
})
);
}
}
return Promise.resolve(response);
}
/**
* Reuses the headers of the original request and url (if specified).
* @param {WebResourceLike} originalRequest The original request
* @param {boolean} reuseUrlToo Should the url from the original request be reused as well. Default false.
* @returns {object} A new request object with desired headers.
*/
function getRequestEssentials(
originalRequest: WebResourceLike,
reuseUrlToo = false
): WebResourceLike {
const reqOptions: WebResourceLike = originalRequest.clone();
if (reuseUrlToo) {
reqOptions.url = originalRequest.url;
}
// We have to change the x-ms-client-request-id otherwise Azure endpoint
// will return the initial 409 (cached) response.
reqOptions.headers.set("x-ms-client-request-id", utils.generateUuid());
// Set content-type to application/json
reqOptions.headers.set("Content-Type", "application/json; charset=utf-8");
return reqOptions;
}
/**
* Validates the error code and message associated with 409 response status code. If it matches to that of
* RP not registered then it returns the name of the RP else returns undefined.
* @param {string} body The response body received after making the original request.
* @returns {string} The name of the RP if condition is satisfied else undefined.
*/
function checkRPNotRegisteredError(body: string): string {
let result, responseBody;
if (body) {
try {
responseBody = JSON.parse(body);
} catch (err) {
// do nothing;
}
if (
responseBody &&
responseBody.error &&
responseBody.error.message &&
responseBody.error.code &&
responseBody.error.code === "MissingSubscriptionRegistration"
) {
const matchRes = responseBody.error.message.match(/.*'(.*)'/i);
if (matchRes) {
result = matchRes.pop();
}
}
}
return result;
}
/**
* Extracts the first part of the URL, just after subscription:
* https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/
* @param {string} url The original request url
* @returns {string} The url prefix as explained above.
*/
function extractSubscriptionUrl(url: string): string {
let result;
const matchRes = url.match(/.*\/subscriptions\/[a-f0-9-]+\//gi);
if (matchRes && matchRes[0]) {
result = matchRes[0];
} else {
throw new Error(`Unable to extract subscriptionId from the given url - ${url}.`);
}
return result;
}
/**
* Registers the given provider.
* @param {RPRegistrationPolicy} policy The RPRegistrationPolicy this function is being called against.
* @param {string} urlPrefix https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/
* @param {string} provider The provider name to be registered.
* @param {WebResourceLike} originalRequest The original request sent by the user that returned a 409 response
* with a message that the provider is not registered.
* @param {registrationCallback} callback The callback that handles the RP registration
*/
function registerRP(
policy: RPRegistrationPolicy,
urlPrefix: string,
provider: string,
originalRequest: WebResourceLike
): Promise<boolean> {
const postUrl = `${urlPrefix}providers/${provider}/register?api-version=2016-02-01`;
const getUrl = `${urlPrefix}providers/${provider}?api-version=2016-02-01`;
const reqOptions = getRequestEssentials(originalRequest);
reqOptions.method = "POST";
reqOptions.url = postUrl;
return policy._nextPolicy.sendRequest(reqOptions).then((response) => {
if (response.status !== 200) {
throw new Error(`Autoregistration of ${provider} failed. Please try registering manually.`);
}
return getRegistrationStatus(policy, getUrl, originalRequest);
});
}
/**
* Polls the registration status of the provider that was registered. Polling happens at an interval of 30 seconds.
* Polling will happen till the registrationState property of the response body is "Registered".
* @param {RPRegistrationPolicy} policy The RPRegistrationPolicy this function is being called against.
* @param {string} url The request url for polling
* @param {WebResourceLike} originalRequest The original request sent by the user that returned a 409 response
* with a message that the provider is not registered.
* @returns {Promise<boolean>} True if RP Registration is successful.
*/
function getRegistrationStatus(
policy: RPRegistrationPolicy,
url: string,
originalRequest: WebResourceLike
): Promise<boolean> {
const reqOptions: any = getRequestEssentials(originalRequest);
reqOptions.url = url;
reqOptions.method = "GET";
return policy._nextPolicy.sendRequest(reqOptions).then((res) => {
const obj = res.parsedBody as any;
if (res.parsedBody && obj.registrationState && obj.registrationState === "Registered") {
return true;
} else {
return utils
.delay(policy._retryTimeout * 1000)
.then(() => getRegistrationStatus(policy, url, originalRequest));
}
});
}

View file

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { ServiceClientCredentials } from "../credentials/serviceClientCredentials";
import { HttpOperationResponse } from "../httpOperationResponse";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicyFactory,
RequestPolicy,
RequestPolicyOptionsLike,
} from "./requestPolicy";
export function signingPolicy(
authenticationProvider: ServiceClientCredentials
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new SigningPolicy(nextPolicy, options, authenticationProvider);
},
};
}
export class SigningPolicy extends BaseRequestPolicy {
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
public authenticationProvider: ServiceClientCredentials
) {
super(nextPolicy, options);
}
signRequest(request: WebResourceLike): Promise<WebResourceLike> {
return this.authenticationProvider.signRequest(request);
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this.signRequest(request).then((nextRequest) =>
this._nextPolicy.sendRequest(nextRequest)
);
}
}

View file

@ -0,0 +1,187 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpOperationResponse } from "../httpOperationResponse";
import * as utils from "../util/utils";
import { WebResourceLike } from "../webResource";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
export interface RetryData {
retryCount: number;
retryInterval: number;
error?: RetryError;
}
export interface RetryError extends Error {
message: string;
code?: string;
innerError?: RetryError;
}
export function systemErrorRetryPolicy(
retryCount?: number,
retryInterval?: number,
minRetryInterval?: number,
maxRetryInterval?: number
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new SystemErrorRetryPolicy(
nextPolicy,
options,
retryCount,
retryInterval,
minRetryInterval,
maxRetryInterval
);
},
};
}
/**
* @class
* Instantiates a new "ExponentialRetryPolicyFilter" instance.
*
* @constructor
* @param {number} retryCount The client retry count.
* @param {number} retryInterval The client retry interval, in milliseconds.
* @param {number} minRetryInterval The minimum retry interval, in milliseconds.
* @param {number} maxRetryInterval The maximum retry interval, in milliseconds.
*/
export class SystemErrorRetryPolicy extends BaseRequestPolicy {
retryCount: number;
retryInterval: number;
minRetryInterval: number;
maxRetryInterval: number;
DEFAULT_CLIENT_RETRY_INTERVAL = 1000 * 30;
DEFAULT_CLIENT_RETRY_COUNT = 3;
DEFAULT_CLIENT_MAX_RETRY_INTERVAL = 1000 * 90;
DEFAULT_CLIENT_MIN_RETRY_INTERVAL = 1000 * 3;
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptionsLike,
retryCount?: number,
retryInterval?: number,
minRetryInterval?: number,
maxRetryInterval?: number
) {
super(nextPolicy, options);
this.retryCount = typeof retryCount === "number" ? retryCount : this.DEFAULT_CLIENT_RETRY_COUNT;
this.retryInterval =
typeof retryInterval === "number" ? retryInterval : this.DEFAULT_CLIENT_RETRY_INTERVAL;
this.minRetryInterval =
typeof minRetryInterval === "number"
? minRetryInterval
: this.DEFAULT_CLIENT_MIN_RETRY_INTERVAL;
this.maxRetryInterval =
typeof maxRetryInterval === "number"
? maxRetryInterval
: this.DEFAULT_CLIENT_MAX_RETRY_INTERVAL;
}
public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy
.sendRequest(request.clone())
.catch((error) => retry(this, request, error.response, error));
}
}
/**
* Determines if the operation should be retried and how long to wait until the next retry.
*
* @param {number} statusCode The HTTP status code.
* @param {RetryData} retryData The retry data.
* @return {boolean} True if the operation qualifies for a retry; false otherwise.
*/
function shouldRetry(policy: SystemErrorRetryPolicy, retryData: RetryData): boolean {
let currentCount;
if (!retryData) {
throw new Error("retryData for the SystemErrorRetryPolicyFilter cannot be null.");
} else {
currentCount = retryData && retryData.retryCount;
}
return currentCount < policy.retryCount;
}
/**
* Updates the retry data for the next attempt.
*
* @param {RetryData} retryData The retry data.
* @param {object} err The operation"s error, if any.
*/
function updateRetryData(
policy: SystemErrorRetryPolicy,
retryData?: RetryData,
err?: RetryError
): RetryData {
if (!retryData) {
retryData = {
retryCount: 0,
retryInterval: 0,
};
}
if (err) {
if (retryData.error) {
err.innerError = retryData.error;
}
retryData.error = err;
}
// Adjust retry count
retryData.retryCount++;
// Adjust retry interval
let incrementDelta = Math.pow(2, retryData.retryCount) - 1;
const boundedRandDelta =
policy.retryInterval * 0.8 + Math.floor(Math.random() * (policy.retryInterval * 0.4));
incrementDelta *= boundedRandDelta;
retryData.retryInterval = Math.min(
policy.minRetryInterval + incrementDelta,
policy.maxRetryInterval
);
return retryData;
}
async function retry(
policy: SystemErrorRetryPolicy,
request: WebResourceLike,
operationResponse: HttpOperationResponse,
err?: RetryError,
retryData?: RetryData
): Promise<HttpOperationResponse> {
retryData = updateRetryData(policy, retryData, err);
if (
err &&
err.code &&
shouldRetry(policy, retryData) &&
(err.code === "ETIMEDOUT" ||
err.code === "ESOCKETTIMEDOUT" ||
err.code === "ECONNREFUSED" ||
err.code === "ECONNRESET" ||
err.code === "ENOENT")
) {
// If previous operation ended with an error and the policy allows a retry, do that
try {
await utils.delay(retryData.retryInterval);
return policy._nextPolicy.sendRequest(request.clone());
} catch (error) {
return retry(policy, request, operationResponse, error, retryData);
}
} else {
if (err) {
// If the operation failed in the end, return all errors instead of just the last one
return Promise.reject(retryData.error);
}
return operationResponse;
}
}

View file

@ -0,0 +1,105 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyOptionsLike,
RequestPolicyFactory,
} from "./requestPolicy";
import { WebResourceLike } from "../webResource";
import { HttpOperationResponse } from "../httpOperationResponse";
import { Constants } from "../util/constants";
import { delay } from "../util/utils";
const StatusCodes = Constants.HttpConstants.StatusCodes;
const DEFAULT_RETRY_COUNT = 3;
/**
* Options that control how to retry on response status code 429.
*/
export interface ThrottlingRetryOptions {
/**
* The maximum number of retry attempts. Defaults to 3.
*/
maxRetries?: number;
}
export function throttlingRetryPolicy(
maxRetries: number = DEFAULT_RETRY_COUNT
): RequestPolicyFactory {
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new ThrottlingRetryPolicy(nextPolicy, options, maxRetries);
},
};
}
/**
* To learn more, please refer to
* https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-request-limits,
* https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits and
* https://docs.microsoft.com/en-us/azure/virtual-machines/troubleshooting/troubleshooting-throttling-errors
*/
export class ThrottlingRetryPolicy extends BaseRequestPolicy {
private retryLimit: number;
constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike, retryLimit: number) {
super(nextPolicy, options);
this.retryLimit = retryLimit;
}
public async sendRequest(httpRequest: WebResourceLike): Promise<HttpOperationResponse> {
return this._nextPolicy.sendRequest(httpRequest.clone()).then((response) => {
return this.retry(httpRequest, response, 0);
});
}
private async retry(
httpRequest: WebResourceLike,
httpResponse: HttpOperationResponse,
retryCount: number
): Promise<HttpOperationResponse> {
if (httpResponse.status !== StatusCodes.TooManyRequests) {
return httpResponse;
}
const retryAfterHeader: string | undefined = httpResponse.headers.get(
Constants.HeaderConstants.RETRY_AFTER
);
if (retryAfterHeader && retryCount < this.retryLimit) {
const delayInMs: number | undefined = ThrottlingRetryPolicy.parseRetryAfterHeader(
retryAfterHeader
);
if (delayInMs) {
await delay(delayInMs);
const res = await this._nextPolicy.sendRequest(httpRequest);
return this.retry(httpRequest, res, retryCount + 1);
}
}
return httpResponse;
}
public static parseRetryAfterHeader(headerValue: string): number | undefined {
const retryAfterInSeconds = Number(headerValue);
if (Number.isNaN(retryAfterInSeconds)) {
return ThrottlingRetryPolicy.parseDateRetryAfterHeader(headerValue);
} else {
return retryAfterInSeconds * 1000;
}
}
public static parseDateRetryAfterHeader(headerValue: string): number | undefined {
try {
const now: number = Date.now();
const date: number = Date.parse(headerValue);
const diff = date - now;
return Number.isNaN(diff) ? undefined : diff;
} catch (error) {
return undefined;
}
}
}

View file

@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { HttpHeaders } from "../httpHeaders";
import { HttpOperationResponse } from "../httpOperationResponse";
import { Constants } from "../util/constants";
import { WebResourceLike } from "../webResource";
import { getDefaultUserAgentKey, getPlatformSpecificData } from "./msRestUserAgentPolicy";
import {
BaseRequestPolicy,
RequestPolicy,
RequestPolicyFactory,
RequestPolicyOptionsLike,
} from "./requestPolicy";
export type TelemetryInfo = { key?: string; value?: string };
function getRuntimeInfo(): TelemetryInfo[] {
const msRestRuntime = {
key: "ms-rest-js",
value: Constants.msRestVersion,
};
return [msRestRuntime];
}
function getUserAgentString(
telemetryInfo: TelemetryInfo[],
keySeparator = " ",
valueSeparator = "/"
): string {
return telemetryInfo
.map((info) => {
const value = info.value ? `${valueSeparator}${info.value}` : "";
return `${info.key}${value}`;
})
.join(keySeparator);
}
export const getDefaultUserAgentHeaderName = getDefaultUserAgentKey;
export function getDefaultUserAgentValue(): string {
const runtimeInfo = getRuntimeInfo();
const platformSpecificData = getPlatformSpecificData();
const userAgent = getUserAgentString(runtimeInfo.concat(platformSpecificData));
return userAgent;
}
export function userAgentPolicy(userAgentData?: TelemetryInfo): RequestPolicyFactory {
const key: string =
!userAgentData || userAgentData.key == undefined ? getDefaultUserAgentKey() : userAgentData.key;
const value: string =
!userAgentData || userAgentData.value == undefined
? getDefaultUserAgentValue()
: userAgentData.value;
return {
create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
return new UserAgentPolicy(nextPolicy, options, key, value);
},
};
}
export class UserAgentPolicy extends BaseRequestPolicy {
constructor(
readonly _nextPolicy: RequestPolicy,
readonly _options: RequestPolicyOptionsLike,
protected headerKey: string,
protected headerValue: string
) {
super(_nextPolicy, _options);
}
sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
this.addUserAgentHeader(request);
return this._nextPolicy.sendRequest(request);
}
addUserAgentHeader(request: WebResourceLike): void {
if (!request.headers) {
request.headers = new HttpHeaders();
}
if (!request.headers.get(this.headerKey) && this.headerValue) {
request.headers.set(this.headerKey, this.headerValue);
}
}
}