UNPKG

7.02 kBPlain TextView Raw
1/**
2 * -------------------------------------------------------------------------------------------
3 * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
4 * See License in the project root for license information.
5 * -------------------------------------------------------------------------------------------
6 */
7
8/**
9 * @module RetryHandler
10 */
11
12import { Context } from "../IContext";
13import { FetchOptions } from "../IFetchOptions";
14import { RequestMethod } from "../RequestMethod";
15import { Middleware } from "./IMiddleware";
16import { MiddlewareControl } from "./MiddlewareControl";
17import { getRequestHeader, setRequestHeader } from "./MiddlewareUtil";
18import { RetryHandlerOptions } from "./options/RetryHandlerOptions";
19import { FeatureUsageFlag, TelemetryHandlerOptions } from "./options/TelemetryHandlerOptions";
20
21/**
22 * @class
23 * @implements Middleware
24 * Class for RetryHandler
25 */
26export class RetryHandler implements Middleware {
27 /**
28 * @private
29 * @static
30 * A list of status codes that needs to be retried
31 */
32 private static RETRY_STATUS_CODES: number[] = [
33 429, // Too many requests
34 503, // Service unavailable
35 504, // Gateway timeout
36 ];
37
38 /**
39 * @private
40 * @static
41 * A member holding the name of retry attempt header
42 */
43 private static RETRY_ATTEMPT_HEADER = "Retry-Attempt";
44
45 /**
46 * @private
47 * @static
48 * A member holding the name of retry after header
49 */
50 private static RETRY_AFTER_HEADER = "Retry-After";
51
52 /**
53 * @private
54 * A member to hold next middleware in the middleware chain
55 */
56 private nextMiddleware: Middleware;
57
58 /**
59 * @private
60 * A member holding the retry handler options
61 */
62 private options: RetryHandlerOptions;
63
64 /**
65 * @public
66 * @constructor
67 * To create an instance of RetryHandler
68 * @param {RetryHandlerOptions} [options = new RetryHandlerOptions()] - The retry handler options value
69 * @returns An instance of RetryHandler
70 */
71 public constructor(options: RetryHandlerOptions = new RetryHandlerOptions()) {
72 this.options = options;
73 }
74
75 /**
76 *
77 * @private
78 * To check whether the response has the retry status code
79 * @param {Response} response - The response object
80 * @returns Whether the response has retry status code or not
81 */
82 private isRetry(response: Response): boolean {
83 return RetryHandler.RETRY_STATUS_CODES.indexOf(response.status) !== -1;
84 }
85
86 /**
87 * @private
88 * To check whether the payload is buffered or not
89 * @param {RequestInfo} request - The url string or the request object value
90 * @param {FetchOptions} options - The options of a request
91 * @returns Whether the payload is buffered or not
92 */
93 private isBuffered(request: RequestInfo, options: FetchOptions | undefined): boolean {
94 const method = typeof request === "string" ? options.method : (request as Request).method;
95 const isPutPatchOrPost: boolean = method === RequestMethod.PUT || method === RequestMethod.PATCH || method === RequestMethod.POST;
96 if (isPutPatchOrPost) {
97 const isStream = getRequestHeader(request, options, "Content-Type") === "application/octet-stream";
98 if (isStream) {
99 return false;
100 }
101 }
102 return true;
103 }
104
105 /**
106 * @private
107 * To get the delay for a retry
108 * @param {Response} response - The response object
109 * @param {number} retryAttempts - The current attempt count
110 * @param {number} delay - The delay value in seconds
111 * @returns A delay for a retry
112 */
113 private getDelay(response: Response, retryAttempts: number, delay: number): number {
114 const getRandomness = () => Number(Math.random().toFixed(3));
115 const retryAfter = response.headers !== undefined ? response.headers.get(RetryHandler.RETRY_AFTER_HEADER) : null;
116 let newDelay: number;
117 if (retryAfter !== null) {
118 if (Number.isNaN(Number(retryAfter))) {
119 newDelay = Math.round((new Date(retryAfter).getTime() - Date.now()) / 1000);
120 } else {
121 newDelay = Number(retryAfter);
122 }
123 } else {
124 // Adding randomness to avoid retrying at a same
125 newDelay = retryAttempts >= 2 ? this.getExponentialBackOffTime(retryAttempts) + delay + getRandomness() : delay + getRandomness();
126 }
127 return Math.min(newDelay, this.options.getMaxDelay() + getRandomness());
128 }
129
130 /**
131 * @private
132 * To get an exponential back off value
133 * @param {number} attempts - The current attempt count
134 * @returns An exponential back off value
135 */
136 private getExponentialBackOffTime(attempts: number): number {
137 return Math.round((1 / 2) * (2 ** attempts - 1));
138 }
139
140 /**
141 * @private
142 * @async
143 * To add delay for the execution
144 * @param {number} delaySeconds - The delay value in seconds
145 * @returns Nothing
146 */
147 private async sleep(delaySeconds: number): Promise<void> {
148 const delayMilliseconds = delaySeconds * 1000;
149 return new Promise((resolve) => setTimeout(resolve, delayMilliseconds));
150 }
151
152 private getOptions(context: Context): RetryHandlerOptions {
153 let options: RetryHandlerOptions;
154 if (context.middlewareControl instanceof MiddlewareControl) {
155 options = context.middlewareControl.getMiddlewareOptions(this.options.constructor) as RetryHandlerOptions;
156 }
157 if (typeof options === "undefined") {
158 options = Object.assign(new RetryHandlerOptions(), this.options);
159 }
160 return options;
161 }
162
163 /**
164 * @private
165 * @async
166 * To execute the middleware with retries
167 * @param {Context} context - The context object
168 * @param {number} retryAttempts - The current attempt count
169 * @param {RetryHandlerOptions} options - The retry middleware options instance
170 * @returns A Promise that resolves to nothing
171 */
172 private async executeWithRetry(context: Context, retryAttempts: number, options: RetryHandlerOptions): Promise<void> {
173 await this.nextMiddleware.execute(context);
174 if (retryAttempts < options.maxRetries && this.isRetry(context.response) && this.isBuffered(context.request, context.options) && options.shouldRetry(options.delay, retryAttempts, context.request, context.options, context.response)) {
175 ++retryAttempts;
176 setRequestHeader(context.request, context.options, RetryHandler.RETRY_ATTEMPT_HEADER, retryAttempts.toString());
177 const delay = this.getDelay(context.response, retryAttempts, options.delay);
178 await this.sleep(delay);
179 return await this.executeWithRetry(context, retryAttempts, options);
180 } else {
181 return;
182 }
183 }
184
185 /**
186 * @public
187 * @async
188 * To execute the current middleware
189 * @param {Context} context - The context object of the request
190 * @returns A Promise that resolves to nothing
191 */
192 public async execute(context: Context): Promise<void> {
193 const retryAttempts = 0;
194 const options: RetryHandlerOptions = this.getOptions(context);
195 TelemetryHandlerOptions.updateFeatureUsageFlag(context, FeatureUsageFlag.RETRY_HANDLER_ENABLED);
196 return await this.executeWithRetry(context, retryAttempts, options);
197 }
198
199 /**
200 * @public
201 * To set the next middleware in the chain
202 * @param {Middleware} next - The middleware instance
203 * @returns Nothing
204 */
205 public setNext(next: Middleware): void {
206 this.nextMiddleware = next;
207 }
208}