Skip to content

Commit a38203f

Browse files
[core-rest-pipeline] Support "retry-after-ms" and "x-ms-retry-after-ms" headers with the throttling policy (Azure#20817)
* expand throttlingRetryStrategy with "retry-after-ms", "x-ms-retry-after-ms" headers * Expand test suite with "retry-after-ms" and "x-ms-retry-after-ms" * changelog * Update sdk/core/core-rest-pipeline/src/retryStrategies/throttlingRetryStrategy.ts * no new variable * no internal tag * refactor * new lines for readability * more refactor * Update sdk/core/core-rest-pipeline/src/retryStrategies/throttlingRetryStrategy.ts * part of the feedback * lint * Update sdk/core/core-rest-pipeline/src/retryStrategies/throttlingRetryStrategy.ts Co-authored-by: Jeff Fisher <[email protected]> * format Co-authored-by: Jeff Fisher <[email protected]>
1 parent e474d5b commit a38203f

File tree

5 files changed

+146
-53
lines changed

5 files changed

+146
-53
lines changed

sdk/core/core-rest-pipeline/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- Supports the `"retry-after-ms"` and `"x-ms-retry-after-ms"` headers along with the `"Retry-After"` header from throttling retry responses from the services. [#20817](https://github.com/Azure/azure-sdk-for-js/issues/20817)
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/core/core-rest-pipeline/src/policies/retryPolicy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export function retryPolicy(
109109
throw errorToThrow;
110110
}
111111

112-
if (retryAfterInMs) {
112+
if (retryAfterInMs || retryAfterInMs === 0) {
113113
strategyLogger.info(
114114
`Retry ${retryCount}: Retry strategy ${strategy.name} retries after ${retryAfterInMs}`
115115
);

sdk/core/core-rest-pipeline/src/retryStrategies/throttlingRetryStrategy.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,77 @@
22
// Licensed under the MIT license.
33

44
import { PipelineResponse } from "..";
5+
import { parseHeaderValueAsNumber } from "../util/helpers";
56
import { RetryStrategy } from "./retryStrategy";
67

78
/**
8-
* Returns the number of milliseconds to wait based on a Retry-After header value.
9-
* Returns undefined if there is no valid value.
10-
* @param headerValue - An HTTP Retry-After header value.
9+
* The header that comes back from Azure services representing
10+
* the amount of time (minimum) to wait to retry (in seconds or timestamp after which we can retry).
1111
*/
12-
function parseRetryAfterHeader(headerValue: string): number | undefined {
12+
const RetryAfterHeader = "Retry-After";
13+
/**
14+
* The headers that come back from Azure services representing
15+
* the amount of time (minimum) to wait to retry.
16+
*
17+
* "retry-after-ms", "x-ms-retry-after-ms" : milliseconds
18+
* "Retry-After" : seconds or timestamp
19+
*/
20+
const AllRetryAfterHeaders: string[] = ["retry-after-ms", "x-ms-retry-after-ms", RetryAfterHeader];
21+
22+
/**
23+
* A response is a throttling retry response if it has a throttling status code (429 or 503),
24+
* as long as one of the [ "Retry-After" or "retry-after-ms" or "x-ms-retry-after-ms" ] headers has a valid value.
25+
*
26+
* Returns the `retryAfterInMs` value if the response is a throttling retry response.
27+
* If not throttling retry response, returns `undefined`.
28+
*
29+
* @internal
30+
*/
31+
function getRetryAfterInMs(response?: PipelineResponse): number | undefined {
32+
if (!(response && [429, 503].includes(response.status))) return undefined;
1333
try {
14-
const retryAfterInSeconds = Number(headerValue);
15-
if (!Number.isNaN(retryAfterInSeconds)) {
16-
return retryAfterInSeconds * 1000;
17-
} else {
18-
// It might be formatted as a date instead of a number of seconds
34+
// Headers: "retry-after-ms", "x-ms-retry-after-ms", "Retry-After"
35+
for (const header of AllRetryAfterHeaders) {
36+
const retryAfterValue = parseHeaderValueAsNumber(response, header);
37+
if (retryAfterValue === 0 || retryAfterValue) {
38+
// "Retry-After" header ==> seconds
39+
// "retry-after-ms", "x-ms-retry-after-ms" headers ==> milli-seconds
40+
const multiplyingFactor = header === RetryAfterHeader ? 1000 : 1;
41+
return retryAfterValue * multiplyingFactor; // in milli-seconds
42+
}
43+
}
1944

20-
const now: number = Date.now();
21-
const date: number = Date.parse(headerValue);
22-
const diff = date - now;
45+
// RetryAfterHeader ("Retry-After") has a special case where it might be formatted as a date instead of a number of seconds
46+
const retryAfterHeader = response.headers.get(RetryAfterHeader);
47+
if (!retryAfterHeader) return;
2348

24-
return Number.isNaN(diff) ? undefined : diff;
25-
}
49+
const date = Date.parse(retryAfterHeader);
50+
const diff = date - Date.now();
51+
// negative diff would mean a date in the past, so retry asap with 0 milliseconds
52+
return Number.isFinite(diff) ? Math.max(0, diff) : undefined;
2653
} catch (e) {
2754
return undefined;
2855
}
2956
}
3057

3158
/**
3259
* A response is a retry response if it has a throttling status code (429 or 503),
33-
* as long as the Retry-After header has a valid value.
60+
* as long as one of the [ "Retry-After" or "retry-after-ms" or "x-ms-retry-after-ms" ] headers has a valid value.
3461
*/
3562
export function isThrottlingRetryResponse(response?: PipelineResponse): boolean {
36-
return Boolean(
37-
response &&
38-
(response.status === 429 || response.status === 503) &&
39-
response.headers.get("Retry-After") &&
40-
parseRetryAfterHeader(response.headers.get("Retry-After")!)
41-
);
63+
return Number.isFinite(getRetryAfterInMs(response));
4264
}
4365

4466
export function throttlingRetryStrategy(): RetryStrategy {
4567
return {
4668
name: "throttlingRetryStrategy",
4769
retry({ response }) {
48-
if (!isThrottlingRetryResponse(response)) {
70+
const retryAfterInMs = getRetryAfterInMs(response);
71+
if (!Number.isFinite(retryAfterInMs)) {
4972
return { skipStrategy: true };
5073
}
5174
return {
52-
retryAfterInMs: parseRetryAfterHeader(response!.headers.get("Retry-After")!),
75+
retryAfterInMs,
5376
};
5477
},
5578
};

sdk/core/core-rest-pipeline/src/util/helpers.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
import { AbortError, AbortSignalLike } from "@azure/abort-controller";
5+
import { PipelineResponse } from "../interfaces";
56

67
/**
78
* A constant that indicates whether the environment the code is running is Node.JS.
@@ -106,3 +107,18 @@ export function isObject(input: unknown): input is UnknownObject {
106107
!(input instanceof Date)
107108
);
108109
}
110+
111+
/**
112+
* @internal
113+
* @returns the parsed value or undefined if the parsed value is invalid.
114+
*/
115+
export function parseHeaderValueAsNumber(
116+
response: PipelineResponse,
117+
headerName: string
118+
): number | undefined {
119+
const value = response.headers.get(headerName);
120+
if (!value) return;
121+
const valueAsNum = Number(value);
122+
if (Number.isNaN(valueAsNum)) return;
123+
return valueAsNum;
124+
}

sdk/core/core-rest-pipeline/test/throttlingRetryPolicy.spec.ts

Lines changed: 81 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,68 @@ describe("throttlingRetryPolicy", function () {
2121
sinon.restore();
2222
});
2323

24-
it("It should retry after a given number of seconds on a response with status code 429", async () => {
24+
const defaultDurations = [0, 10 * 1000]; // milliseconds
25+
26+
defaultDurations.forEach((defaultDuration) => {
27+
const headersWithDefaultDuration = [
28+
{
29+
"Retry-After": String(defaultDuration / 1000),
30+
},
31+
{
32+
"retry-after-ms": String(defaultDuration),
33+
},
34+
{
35+
"x-ms-retry-after-ms": String(defaultDuration),
36+
},
37+
] as const;
38+
headersWithDefaultDuration.forEach((headers) => {
39+
it(`(${
40+
Object.keys(headers)[0]
41+
}) - should retry after a given number of seconds/milliseconds on a response with status code 429`, async () => {
42+
const request = createPipelineRequest({
43+
url: "https://bing.com",
44+
});
45+
const retryResponse: PipelineResponse = {
46+
headers: createHttpHeaders(headers),
47+
request,
48+
status: 429,
49+
};
50+
const successResponse: PipelineResponse = {
51+
headers: createHttpHeaders(),
52+
request,
53+
status: 200,
54+
};
55+
56+
const policy = throttlingRetryPolicy();
57+
const next = sinon.stub<Parameters<SendRequest>, ReturnType<SendRequest>>();
58+
next.onFirstCall().resolves(retryResponse);
59+
next.onSecondCall().resolves(successResponse);
60+
61+
const clock = sinon.useFakeTimers();
62+
63+
const promise = policy.sendRequest(request, next);
64+
assert.isTrue(next.calledOnce, "next wasn't called once");
65+
66+
// allow the delay to occur
67+
const time = await clock.nextAsync();
68+
assert.strictEqual(time, defaultDuration);
69+
assert.isTrue(next.calledTwice, "next wasn't called twice");
70+
71+
const result = await promise;
72+
73+
assert.strictEqual(result, successResponse);
74+
clock.restore();
75+
});
76+
});
77+
});
78+
79+
it("It should retry after a given date occurs on a response with status code 429", async () => {
2580
const request = createPipelineRequest({
2681
url: "https://bing.com",
2782
});
2883
const retryResponse: PipelineResponse = {
2984
headers: createHttpHeaders({
30-
"Retry-After": "10",
85+
"Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT",
3186
}),
3287
request,
3388
status: 429,
@@ -43,14 +98,18 @@ describe("throttlingRetryPolicy", function () {
4398
next.onFirstCall().resolves(retryResponse);
4499
next.onSecondCall().resolves(successResponse);
45100

46-
const clock = sinon.useFakeTimers();
101+
const clock = sinon.useFakeTimers(new Date("Wed, 21 Oct 2015 07:20:00 GMT"));
47102

48103
const promise = policy.sendRequest(request, next);
49104
assert.isTrue(next.calledOnce);
50105

51106
// allow the delay to occur
52107
const time = await clock.nextAsync();
53-
assert.strictEqual(time, 10 * 1000);
108+
assert.strictEqual(
109+
time,
110+
new Date("Wed, 21 Oct 2015 07:28:00 GMT").getTime(),
111+
"It should now be the time from the header."
112+
);
54113
assert.isTrue(next.calledTwice);
55114

56115
const result = await promise;
@@ -59,16 +118,16 @@ describe("throttlingRetryPolicy", function () {
59118
clock.restore();
60119
});
61120

62-
it("It should retry after a given date occurs on a response with status code 429", async () => {
121+
it("It should retry after a given number of seconds on a response with status code 503", async () => {
63122
const request = createPipelineRequest({
64123
url: "https://bing.com",
65124
});
66125
const retryResponse: PipelineResponse = {
67126
headers: createHttpHeaders({
68-
"Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT",
127+
"Retry-After": "10",
69128
}),
70129
request,
71-
status: 429,
130+
status: 503,
72131
};
73132
const successResponse: PipelineResponse = {
74133
headers: createHttpHeaders(),
@@ -81,18 +140,14 @@ describe("throttlingRetryPolicy", function () {
81140
next.onFirstCall().resolves(retryResponse);
82141
next.onSecondCall().resolves(successResponse);
83142

84-
const clock = sinon.useFakeTimers(new Date("Wed, 21 Oct 2015 07:20:00 GMT"));
143+
const clock = sinon.useFakeTimers();
85144

86145
const promise = policy.sendRequest(request, next);
87146
assert.isTrue(next.calledOnce);
88147

89148
// allow the delay to occur
90149
const time = await clock.nextAsync();
91-
assert.strictEqual(
92-
time,
93-
new Date("Wed, 21 Oct 2015 07:28:00 GMT").getTime(),
94-
"It should now be the time from the header."
95-
);
150+
assert.strictEqual(time, 10 * 1000);
96151
assert.isTrue(next.calledTwice);
97152

98153
const result = await promise;
@@ -101,13 +156,13 @@ describe("throttlingRetryPolicy", function () {
101156
clock.restore();
102157
});
103158

104-
it("It should retry after a given number of seconds on a response with status code 503", async () => {
159+
it("It should retry after a given date occurs on a response with status code 503", async () => {
105160
const request = createPipelineRequest({
106161
url: "https://bing.com",
107162
});
108163
const retryResponse: PipelineResponse = {
109164
headers: createHttpHeaders({
110-
"Retry-After": "10",
165+
"Retry-After": "Wed, 21 Oct 2015 07:28:00 GMT",
111166
}),
112167
request,
113168
status: 503,
@@ -123,14 +178,18 @@ describe("throttlingRetryPolicy", function () {
123178
next.onFirstCall().resolves(retryResponse);
124179
next.onSecondCall().resolves(successResponse);
125180

126-
const clock = sinon.useFakeTimers();
181+
const clock = sinon.useFakeTimers(new Date("Wed, 21 Oct 2015 07:20:00 GMT"));
127182

128183
const promise = policy.sendRequest(request, next);
129184
assert.isTrue(next.calledOnce);
130185

131186
// allow the delay to occur
132187
const time = await clock.nextAsync();
133-
assert.strictEqual(time, 10 * 1000);
188+
assert.strictEqual(
189+
time,
190+
new Date("Wed, 21 Oct 2015 07:28:00 GMT").getTime(),
191+
"It should now be the time from the header."
192+
);
134193
assert.isTrue(next.calledTwice);
135194

136195
const result = await promise;
@@ -139,7 +198,7 @@ describe("throttlingRetryPolicy", function () {
139198
clock.restore();
140199
});
141200

142-
it("It should retry after a given date occurs on a response with status code 503", async () => {
201+
it("It should retry after 0 seconds with status code 503 for a past date", async () => {
143202
const request = createPipelineRequest({
144203
url: "https://bing.com",
145204
});
@@ -161,19 +220,12 @@ describe("throttlingRetryPolicy", function () {
161220
next.onFirstCall().resolves(retryResponse);
162221
next.onSecondCall().resolves(successResponse);
163222

164-
const clock = sinon.useFakeTimers(new Date("Wed, 21 Oct 2015 07:20:00 GMT"));
165-
166223
const promise = policy.sendRequest(request, next);
167-
assert.isTrue(next.calledOnce);
224+
const clock = sinon.useFakeTimers();
168225

169-
// allow the delay to occur
170-
const time = await clock.nextAsync();
171-
assert.strictEqual(
172-
time,
173-
new Date("Wed, 21 Oct 2015 07:28:00 GMT").getTime(),
174-
"It should now be the time from the header."
175-
);
176-
assert.isTrue(next.calledTwice);
226+
assert.isTrue(next.calledOnce, "next wasn't called once");
227+
await clock.nextAsync();
228+
assert.isTrue(next.calledTwice, "next wasn't called twice");
177229

178230
const result = await promise;
179231

0 commit comments

Comments
 (0)