diff --git a/lib/policies/exponentialRetryPolicy.ts b/lib/policies/exponentialRetryPolicy.ts index abcace8..145cffc 100644 --- a/lib/policies/exponentialRetryPolicy.ts +++ b/lib/policies/exponentialRetryPolicy.ts @@ -35,7 +35,6 @@ export function exponentialRetryPolicy(retryCount?: number, retryInterval?: numb * @param {number} maxRetryInterval The maximum retry interval, in milliseconds. */ export class ExponentialRetryPolicy extends BaseRequestPolicy { - retryCount: number; retryInterval: number; minRetryInterval: number; @@ -53,91 +52,82 @@ export class ExponentialRetryPolicy extends BaseRequestPolicy { this.maxRetryInterval = typeof maxRetryInterval === "number" ? maxRetryInterval : this.DEFAULT_CLIENT_MAX_RETRY_INTERVAL; } - /** - * 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. - */ - shouldRetry(statusCode: number, retryData: RetryData): boolean { - if ((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 < this.retryCount); - } - - /** - * Updates the retry data for the next attempt. - * - * @param {RetryData} retryData The retry data. - * @param {object} err The operation"s error, if any. - */ - updateRetryData(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 = this.retryInterval * 0.8 + - Math.floor(Math.random() * (this.retryInterval * 1.2 - this.retryInterval * 0.8)); - incrementDelta *= boundedRandDelta; - - retryData.retryInterval = Math.min(this.minRetryInterval + incrementDelta, this.maxRetryInterval); - - return retryData; - } - - async retry(request: WebResource, response: HttpOperationResponse, retryData?: RetryData, requestError?: RetryError): Promise { - retryData = this.updateRetryData(retryData, requestError); - const isAborted: boolean | undefined = request.abortSignal && request.abortSignal.aborted; - if (!isAborted && this.shouldRetry(response.status, retryData)) { - try { - await utils.delay(retryData.retryInterval); - response = await this._nextPolicy.sendRequest(request.clone()); - requestError = undefined; - } catch (err) { - requestError = err; - } - return this.retry(request, response, retryData, requestError); - } else if (isAborted || !utils.objectIsNull(requestError)) { - // If the operation failed in the end, return all errors instead of just the last one - requestError = retryData.error; - return Promise.reject(requestError); - } else { - return Promise.resolve(response); - } - } - - public async sendRequest(request: WebResource): Promise { - try { - const response: HttpOperationResponse = await this._nextPolicy.sendRequest(request.clone()); - return this.retry(request, response); - } catch (error) { - return this.retry(request, error.response, undefined, error); - } + public sendRequest(request: WebResource): Promise { + return this._nextPolicy.sendRequest(request.clone()).then(response => retry(this, request, response)); + } +} + +/** + * 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: ExponentialRetryPolicy, statusCode: number, retryData: RetryData): boolean { + if ((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 {RetryData} retryData The retry data. + * @param {object} 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: WebResource, response: HttpOperationResponse, retryData?: RetryData, requestError?: RetryError): Promise { + retryData = updateRetryData(policy, retryData, requestError); + const isAborted: boolean | undefined = request.abortSignal && request.abortSignal.aborted; + if (!isAborted && shouldRetry(policy, response.status, retryData)) { + return utils.delay(retryData.retryInterval) + .then(() => policy._nextPolicy.sendRequest(request.clone())) + .then(res => retry(policy, request, res, retryData, requestError)) + .catch(err => retry(policy, request, response, retryData, err)); + } else if (isAborted || !utils.objectIsNull(requestError)) { + // If the operation failed in the end, return all errors instead of just the last one + requestError = retryData.error; + return Promise.reject(requestError); + } else { + return Promise.resolve(response); } } diff --git a/lib/policies/logPolicy.ts b/lib/policies/logPolicy.ts index c09852a..a4afcc8 100644 --- a/lib/policies/logPolicy.ts +++ b/lib/policies/logPolicy.ts @@ -12,7 +12,6 @@ export function logPolicy(logger: any = console.log): RequestPolicyCreator { } export class LogPolicy extends BaseRequestPolicy { - logger?: any; constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, logger: any = console.log) { @@ -20,16 +19,15 @@ export class LogPolicy extends BaseRequestPolicy { this.logger = logger; } - public async sendRequest(request: WebResource): Promise { - const response: HttpOperationResponse = await this._nextPolicy.sendRequest(request); - return this.logResponse(response); - } - - public logResponse(response: HttpOperationResponse): Promise { - this.logger(`>> Request: ${JSON.stringify(response.request, undefined, 2)}`); - this.logger(`>> Response status code: ${response.status}`); - const responseBody = response.bodyAsText; - this.logger(`>> Body: ${responseBody}`); - return Promise.resolve(response); + public sendRequest(request: WebResource): Promise { + return this._nextPolicy.sendRequest(request).then(response => logResponse(this, response)); } } + +function logResponse(policy: LogPolicy, response: HttpOperationResponse): Promise { + 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); +} \ No newline at end of file diff --git a/lib/policies/msRestUserAgentPolicy.ts b/lib/policies/msRestUserAgentPolicy.ts index a981614..ab5c4e7 100644 --- a/lib/policies/msRestUserAgentPolicy.ts +++ b/lib/policies/msRestUserAgentPolicy.ts @@ -63,8 +63,8 @@ export class MsRestUserAgentPolicy extends BaseRequestPolicy { } } - public async sendRequest(request: WebResource): Promise { + public sendRequest(request: WebResource): Promise { this.addUserAgentHeader(request); - return await this._nextPolicy.sendRequest(request); + return this._nextPolicy.sendRequest(request); } } diff --git a/lib/policies/redirectPolicy.ts b/lib/policies/redirectPolicy.ts index 76129ac..2bf9c3e 100644 --- a/lib/policies/redirectPolicy.ts +++ b/lib/policies/redirectPolicy.ts @@ -12,48 +12,36 @@ export function redirectPolicy(maximumRetries = 20): RequestPolicyCreator { } export class RedirectPolicy extends BaseRequestPolicy { - - maximumRetries?: number; - - constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, maximumRetries = 20) { + constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, readonly maxRetries = 20) { super(nextPolicy, options); - this.maximumRetries = maximumRetries; } - async handleRedirect(response: HttpOperationResponse, currentRetries: number): Promise { - const request = response.request; - const locationHeader = response.headers.get("location"); - if (locationHeader && - (response.status === 300 || response.status === 307 || (response.status === 303 && request.method === "POST")) && - (!this.maximumRetries || currentRetries < this.maximumRetries)) { - - const builder = URLBuilder.parse(request.url); - builder.setPath(locationHeader); - request.url = builder.toString(); - - // POST request with Status code 303 should be converted into a - // redirected GET request if the redirect url is present in the location header - if (response.status === 303) { - request.method = "GET"; - } - let res: HttpOperationResponse; - try { - res = await this._nextPolicy.sendRequest(request); - currentRetries++; - } catch (err) { - return Promise.reject(err); - } - return this.handleRedirect(res, currentRetries); - } - return Promise.resolve(response); - } - - public async sendRequest(request: WebResource): Promise { - try { - const response: HttpOperationResponse = await this._nextPolicy.sendRequest(request); - return this.handleRedirect(response, 0); - } catch (error) { - return Promise.reject(error); - } + public sendRequest(request: WebResource): Promise { + return this._nextPolicy.sendRequest(request).then(response => handleRedirect(this, response, 0)); } } + + +function handleRedirect(policy: RedirectPolicy, response: HttpOperationResponse, currentRetries: number): Promise { + const { request, status } = response; + const locationHeader = response.headers.get("location"); + if (locationHeader && + (status === 300 || status === 307 || (status === 303 && request.method === "POST")) && + (!policy.maxRetries || currentRetries < policy.maxRetries)) { + + const builder = URLBuilder.parse(request.url); + builder.setPath(locationHeader); + request.url = builder.toString(); + + // POST request with Status code 303 should be converted into a + // redirected GET request if the redirect url is present in the location header + if (status === 303) { + request.method = "GET"; + } + + return policy._nextPolicy.sendRequest(request) + .then(res => handleRedirect(policy, res, currentRetries + 1)); + } + + return Promise.resolve(response); +} \ No newline at end of file diff --git a/lib/policies/requestPolicy.ts b/lib/policies/requestPolicy.ts index 2749f57..88d9c99 100644 --- a/lib/policies/requestPolicy.ts +++ b/lib/policies/requestPolicy.ts @@ -16,7 +16,7 @@ export interface RequestPolicy { } export abstract class BaseRequestPolicy implements RequestPolicy { - protected constructor(protected readonly _nextPolicy: RequestPolicy, protected readonly _options: RequestPolicyOptions) { + protected constructor(readonly _nextPolicy: RequestPolicy, readonly _options: RequestPolicyOptions) { } public abstract sendRequest(webResource: WebResource): Promise; diff --git a/lib/policies/rpRegistrationPolicy.ts b/lib/policies/rpRegistrationPolicy.ts index 183c7c8..ba700f3 100644 --- a/lib/policies/rpRegistrationPolicy.ts +++ b/lib/policies/rpRegistrationPolicy.ts @@ -12,176 +12,156 @@ export function rpRegistrationPolicy(retryTimeout = 30): RequestPolicyCreator { } export class RPRegistrationPolicy extends BaseRequestPolicy { - - constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, private _retryTimeout = 30) { + constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, readonly _retryTimeout = 30) { super(nextPolicy, options); } - public async sendRequest(request: WebResource): Promise { - const response: HttpOperationResponse = await this._nextPolicy.sendRequest(request.clone()); - return this.registerIfNeeded(request, response); + public sendRequest(request: WebResource): Promise { + return this._nextPolicy.sendRequest(request.clone()) + .then(response => registerIfNeeded(this, request, response)); } +} - async registerIfNeeded(request: WebResource, response: HttpOperationResponse): Promise { - let rpName, urlPrefix; - if (response.status === 409) { - rpName = this.checkRPNotRegisteredError(response.bodyAsText as string); - } + +function registerIfNeeded(policy: RPRegistrationPolicy, request: WebResource, response: HttpOperationResponse): Promise { + if (response.status === 409) { + const rpName = checkRPNotRegisteredError(response.bodyAsText as string); if (rpName) { - urlPrefix = this.extractSubscriptionUrl(request.url); - let registrationStatus = false; - try { - registrationStatus = await this.registerRP(urlPrefix, rpName, request); - } catch (err) { + 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. - } - - 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()); - let finalRes: HttpOperationResponse; - try { - finalRes = await this._nextPolicy.sendRequest(request.clone()); - } catch (err) { - return Promise.reject(err); - } - return Promise.resolve(finalRes); - } + .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 {WebResource} originalRequest The original request - * @param {boolean} reuseUrlToo Should the url from the original request be reused as well. Default false. - * @returns {object} reqOptions - A new request object with desired headers. - */ - getRequestEssentials(originalRequest: WebResource, reuseUrlToo = false): any { - const reqOptions: any = { - headers: {} - }; - if (reuseUrlToo) { - reqOptions.url = originalRequest.url; - } + return Promise.resolve(response); +} - // Copy over the original request headers. This will get us the auth token and other useful stuff from - // the original request header. Thus making it easier to make requests from this filter. - for (const h in originalRequest.headers) { - reqOptions.headers.set(h, originalRequest.headers.get(h)); - } - // We have to change the x-ms-client-request-id otherwise Azure endpoint - // will return the initial 409 (cached) response. - reqOptions.headers["x-ms-client-request-id"] = utils.generateUuid(); - - // Set content-type to application/json - reqOptions.headers["Content-Type"] = "application/json; charset=utf-8"; - - return reqOptions; +/** + * Reuses the headers of the original request and url (if specified). + * @param {WebResource} originalRequest The original request + * @param {boolean} reuseUrlToo Should the url from the original request be reused as well. Default false. + * @returns {object} reqOptions - A new request object with desired headers. + */ +function getRequestEssentials(originalRequest: WebResource, reuseUrlToo = false): any { + const reqOptions: any = { + headers: {} + }; + if (reuseUrlToo) { + reqOptions.url = originalRequest.url; } - /** - * 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} result The name of the RP if condition is satisfied else undefined. - */ - 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; + // Copy over the original request headers. This will get us the auth token and other useful stuff from + // the original request header. Thus making it easier to make requests from this filter. + for (const h in originalRequest.headers) { + reqOptions.headers.set(h, originalRequest.headers.get(h)); } + // We have to change the x-ms-client-request-id otherwise Azure endpoint + // will return the initial 409 (cached) response. + reqOptions.headers["x-ms-client-request-id"] = utils.generateUuid(); - /** - * 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} urlPrefix The url prefix as explained above. - */ - extractSubscriptionUrl(url: string): string { - let result; - const matchRes = url.match(/.*\/subscriptions\/[a-f0-9-]+\//ig); - if (matchRes && matchRes[0]) { - result = matchRes[0]; - } else { - throw new Error(`Unable to extract subscriptionId from the given url - ${url}.`); - } - return result; - } + // Set content-type to application/json + reqOptions.headers["Content-Type"] = "application/json; charset=utf-8"; - /** - * Registers the given provider. - * @param {string} urlPrefix - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/ - * @param {string} provider - The provider name to be registered. - * @param {object} 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 - */ - async registerRP(urlPrefix: string, provider: string, originalRequest: WebResource): Promise { - const postUrl = `${urlPrefix}providers/${provider}/register?api-version=2016-02-01`; - const getUrl = `${urlPrefix}providers/${provider}?api-version=2016-02-01`; - const reqOptions = this.getRequestEssentials(originalRequest); - reqOptions.method = "POST"; - reqOptions.url = postUrl; - let response: HttpOperationResponse; + 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} result The name of the RP if condition is satisfied else undefined. + */ +function checkRPNotRegisteredError(body: string): string { + let result, responseBody; + if (body) { try { - response = await this._nextPolicy.sendRequest(reqOptions); + responseBody = JSON.parse(body); } catch (err) { - return Promise.reject(err); + // do nothing; } - if (response.status !== 200) { - return Promise.reject(new Error(`Autoregistration of ${provider} failed. Please try registering manually.`)); + 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(); + } } - let statusRes = false; - try { - statusRes = await this.getRegistrationStatus(getUrl, originalRequest); - } catch (err) { - return Promise.reject(err); - } - return Promise.resolve(statusRes); } + return result; +} - /** - * 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 {string} url - The request url for polling - * @param {object} originalRequest - The original request sent by the user that returned a 409 response - * with a message that the provider is not registered. - * @returns {Promise} promise - True if RP Registration is successful. - */ - async getRegistrationStatus(url: string, originalRequest: WebResource): Promise { - const reqOptions: any = this.getRequestEssentials(originalRequest); - let res: HttpOperationResponse; - let result = false; - reqOptions.url = url; - reqOptions.method = "GET"; - try { - res = await this._nextPolicy.sendRequest(reqOptions); - } catch (err) { - return Promise.reject(err); - } +/** + * 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} urlPrefix The url prefix as explained above. + */ +function extractSubscriptionUrl(url: string): string { + let result; + const matchRes = url.match(/.*\/subscriptions\/[a-f0-9-]+\//ig); + 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 {string} urlPrefix - https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/ + * @param {string} provider - The provider name to be registered. + * @param {object} 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: WebResource): Promise { + 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 {string} url - The request url for polling + * @param {object} originalRequest - The original request sent by the user that returned a 409 response + * with a message that the provider is not registered. + * @returns {Promise} promise - True if RP Registration is successful. + */ +function getRegistrationStatus(policy: RPRegistrationPolicy, url: string, originalRequest: WebResource): Promise { + 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") { - result = true; + return true; } else { - setTimeout(() => { return this.getRegistrationStatus(url, originalRequest); }, this._retryTimeout * 1000); + return utils.delay(policy._retryTimeout * 1000).then(() => getRegistrationStatus(policy, url, originalRequest)); } - return Promise.resolve(result); - } -} \ No newline at end of file + }); +} diff --git a/lib/policies/signingPolicy.ts b/lib/policies/signingPolicy.ts index a339a44..534b04c 100644 --- a/lib/policies/signingPolicy.ts +++ b/lib/policies/signingPolicy.ts @@ -22,8 +22,7 @@ export class SigningPolicy extends BaseRequestPolicy { return this.authenticationProvider.signRequest(request); } - public async sendRequest(request: WebResource): Promise { - const nextRequest: WebResource = await this.signRequest(request); - return await this._nextPolicy.sendRequest(nextRequest); + public sendRequest(request: WebResource): Promise { + return this.signRequest(request).then(nextRequest => this._nextPolicy.sendRequest(nextRequest)); } } diff --git a/lib/policies/systemErrorRetryPolicy.ts b/lib/policies/systemErrorRetryPolicy.ts index e3aa7e1..c35148c 100644 --- a/lib/policies/systemErrorRetryPolicy.ts +++ b/lib/policies/systemErrorRetryPolicy.ts @@ -35,7 +35,6 @@ export function systemErrorRetryPolicy(retryCount?: number, retryInterval?: numb * @param {number} maxRetryInterval The maximum retry interval, in milliseconds. */ export class SystemErrorRetryPolicy extends BaseRequestPolicy { - retryCount: number; retryInterval: number; minRetryInterval: number; @@ -53,85 +52,80 @@ export class SystemErrorRetryPolicy extends BaseRequestPolicy { this.maxRetryInterval = typeof maxRetryInterval === "number" ? maxRetryInterval : this.DEFAULT_CLIENT_MAX_RETRY_INTERVAL; } - /** - * 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. - */ - shouldRetry(retryData: RetryData): boolean { - let currentCount; - if (!retryData) { - throw new Error("retryData for the SystemErrorRetryPolicyFilter cannot be null."); - } else { - currentCount = (retryData && retryData.retryCount); - } - return (currentCount < this.retryCount); - } - - /** - * Updates the retry data for the next attempt. - * - * @param {RetryData} retryData The retry data. - * @param {object} err The operation"s error, if any. - */ - updateRetryData(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 = this.retryInterval * 0.8 + - Math.floor(Math.random() * (this.retryInterval * 1.2 - this.retryInterval * 0.8)); - incrementDelta *= boundedRandDelta; - - retryData.retryInterval = Math.min(this.minRetryInterval + incrementDelta, this.maxRetryInterval); - - return retryData; - } - - async retry(request: WebResource, operationResponse: HttpOperationResponse, retryData?: RetryData, err?: RetryError): Promise { - const self = this; - retryData = self.updateRetryData(retryData, err); - if (err && err.code && self.shouldRetry(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); - const res: HttpOperationResponse = await this._nextPolicy.sendRequest(request.clone()); - return self.retry(request, res, retryData, err); - } catch (err) { - return self.retry(request, operationResponse, retryData, err); - } - } else { - if (!utils.objectIsNull(err)) { - // If the operation failed in the end, return all errors instead of just the last one - err = retryData.error; - return Promise.reject(err); - } - return Promise.resolve(operationResponse); - } - } - - public async sendRequest(request: WebResource): Promise { - const response: HttpOperationResponse = await this._nextPolicy.sendRequest(request.clone()); - return this.retry(request, response); // See: https://github.com/Microsoft/TypeScript/issues/7426 + public sendRequest(request: WebResource): Promise { + return this._nextPolicy.sendRequest(request.clone()).then(response => retry(this, request, response)); } } + +/** + * 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 * 1.2 - policy.retryInterval * 0.8)); + incrementDelta *= boundedRandDelta; + + retryData.retryInterval = Math.min(policy.minRetryInterval + incrementDelta, policy.maxRetryInterval); + + return retryData; +} + +function retry(policy: SystemErrorRetryPolicy, request: WebResource, operationResponse: HttpOperationResponse, retryData?: RetryData, err?: RetryError): Promise { + 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 + return utils.delay(retryData.retryInterval) + .then(() => policy._nextPolicy.sendRequest(request.clone())) + .then(res => retry(policy, request, res, retryData, err)) + .catch(err => retry(policy, request, operationResponse, retryData, err)); + } else { + if (err == undefined) { + // If the operation failed in the end, return all errors instead of just the last one + err = retryData.error; + return Promise.reject(err); + } + return Promise.resolve(operationResponse); + } +} \ No newline at end of file diff --git a/lib/serializer.ts b/lib/serializer.ts index 7db5701..5f9d865 100644 --- a/lib/serializer.ts +++ b/lib/serializer.ts @@ -588,16 +588,15 @@ function deserializeCompositeType(serializer: Serializer, mapper: CompositeMappe function deserializeDictionaryType(serializer: Serializer, mapper: DictionaryMapper, responseBody: any, objectName: string): any { /*jshint validthis: true */ - if (!mapper.type.value || typeof mapper.type.value !== "object") { + const value = mapper.type.value; + if (!value || typeof value !== "object") { throw new Error(`"value" metadata for a Dictionary must be defined in the ` + `mapper and it must of type "object" in ${objectName}`); } if (responseBody) { const tempDictionary: { [key: string]: any } = {}; - for (const key in responseBody) { - if (responseBody.hasOwnProperty(key)) { - tempDictionary[key] = serializer.deserialize(mapper.type.value, responseBody[key], objectName); - } + for (const key of Object.keys(responseBody)) { + tempDictionary[key] = serializer.deserialize(value, responseBody[key], objectName); } return tempDictionary; } @@ -606,7 +605,8 @@ function deserializeDictionaryType(serializer: Serializer, mapper: DictionaryMap function deserializeSequenceType(serializer: Serializer, mapper: SequenceMapper, responseBody: any, objectName: string): any { /*jshint validthis: true */ - if (!mapper.type.element || typeof mapper.type.element !== "object") { + const element = mapper.type.element; + if (!element || typeof element !== "object") { throw new Error(`element" metadata for an Array must be defined in the ` + `mapper and it must of type "object" in ${objectName}`); } @@ -618,7 +618,7 @@ function deserializeSequenceType(serializer: Serializer, mapper: SequenceMapper, const tempArray = []; for (let i = 0; i < responseBody.length; i++) { - tempArray[i] = serializer.deserialize(mapper.type.element, responseBody[i], objectName); + tempArray[i] = serializer.deserialize(element, responseBody[i], objectName); } return tempArray; } @@ -638,10 +638,11 @@ function getPolymorphicMapper(serializer: Serializer, mapper: CompositeMapper, o // for the model that needs to be serializes or deserialized. // We need this routing for backwards compatibility. This will absorb the breaking change in the mapper and allow new versions // of the runtime to work seamlessly with older version (>= 0.17.0-Nightly20161008) of Autorest generated node.js clients. - if (mapper.type.polymorphicDiscriminator) { - if (typeof mapper.type.polymorphicDiscriminator.valueOf() === "string") { + const polymorphicDiscriminator = mapper.type.polymorphicDiscriminator; + if (polymorphicDiscriminator) { + if (typeof polymorphicDiscriminator.valueOf() === "string") { return getPolymorphicMapperStringVersion(serializer, mapper, object, objectName); - } else if (mapper.type.polymorphicDiscriminator instanceof Object) { + } else if (polymorphicDiscriminator instanceof Object) { return getPolymorphicMapperObjectVersion(serializer, mapper, object, objectName, mode); } else { throw new Error(`The polymorphicDiscriminator for "${objectName}" is neither a string nor an object.`); diff --git a/lib/serviceClient.ts b/lib/serviceClient.ts index ec9fb75..d10241e 100644 --- a/lib/serviceClient.ts +++ b/lib/serviceClient.ts @@ -139,7 +139,7 @@ export class ServiceClient { /** * Send the provided httpRequest. */ - async sendRequest(options: RequestPrepareOptions | WebResource): Promise { + sendRequest(options: RequestPrepareOptions | WebResource): Promise { if (options === null || options === undefined || typeof options !== "object") { throw new Error("options cannot be null or undefined and it must be of type object."); } @@ -157,20 +157,13 @@ export class ServiceClient { return Promise.reject(error); } - // send request - let operationResponse: HttpOperationResponse; - try { - let httpPipeline: RequestPolicy = this._httpClient; - if (this._requestPolicyCreators && this._requestPolicyCreators.length > 0) { - for (let i = this._requestPolicyCreators.length - 1; i >= 0; --i) { - httpPipeline = this._requestPolicyCreators[i](httpPipeline, this._requestPolicyOptions); - } + let httpPipeline: RequestPolicy = this._httpClient; + if (this._requestPolicyCreators && this._requestPolicyCreators.length > 0) { + for (let i = this._requestPolicyCreators.length - 1; i >= 0; --i) { + httpPipeline = this._requestPolicyCreators[i](httpPipeline, this._requestPolicyOptions); } - operationResponse = await httpPipeline.sendRequest(httpRequest); - } catch (err) { - return Promise.reject(err); } - return Promise.resolve(operationResponse); + return httpPipeline.sendRequest(httpRequest); } /** diff --git a/test/shared/logFilterTests.ts b/test/shared/logFilterTests.ts index 141a967..6375c67 100644 --- a/test/shared/logFilterTests.ts +++ b/test/shared/logFilterTests.ts @@ -10,8 +10,7 @@ import { WebResource } from "../../lib/webResource"; const emptyRequestPolicy: RequestPolicy = { sendRequest(request: WebResource): Promise { - assert(request); - throw new Error("Not Implemented"); + return Promise.resolve({ request, status: 200, headers: new HttpHeaders(), bodyAsText: null }); } }; @@ -36,8 +35,7 @@ describe("Log filter", () => { const logger = (message: string): void => { output += message + "\n"; }; const lf = new LogPolicy(emptyRequestPolicy, new RequestPolicyOptions(), logger); const req = new WebResource("https://foo.com", "PUT", { "a": 1 }); - const opRes: HttpOperationResponse = { request: req, status: 200, headers: new HttpHeaders(), bodyAsText: null }; - lf.logResponse(opRes).then(() => { + lf.sendRequest(req).then(() => { // console.dir(output, { depth: null }); // console.log(">>>>>>>"); // console.dir(expected);