Skip to content

Commit f71c136

Browse files
authored
feat: add check for isTransientError (#1222)
1 parent 6a9a1f3 commit f71c136

File tree

8 files changed

+135
-63
lines changed

8 files changed

+135
-63
lines changed

Diff for: packages/middleware-retry/src/constants.ts

-8
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,6 @@ export const DEFAULT_RETRY_DELAY_BASE = 100;
1010
*/
1111
export const MAXIMUM_RETRY_DELAY = 20 * 1000;
1212

13-
/**
14-
* HTTP status codes that indicate the operation may be retried.
15-
*/
16-
export const RETRYABLE_STATUS_CODES = new Set<number>();
17-
[429, 500, 502, 503, 504, 509].forEach(code =>
18-
RETRYABLE_STATUS_CODES.add(code)
19-
);
20-
2113
/**
2214
* The retry delay base (in milliseconds) to use when a throttling error is
2315
* encountered.

Diff for: packages/middleware-retry/src/retryDecider.spec.ts

+42-21
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,58 @@
1+
import {
2+
isClockSkewError,
3+
isThrottlingError,
4+
isTransientError
5+
} from "@aws-sdk/service-error-classification";
16
import { defaultRetryDecider } from "./retryDecider";
27

8+
jest.mock("@aws-sdk/service-error-classification", () => ({
9+
isClockSkewError: jest.fn().mockReturnValue(false),
10+
isThrottlingError: jest.fn().mockReturnValue(false),
11+
isTransientError: jest.fn().mockReturnValue(false)
12+
}));
13+
314
describe("defaultRetryDecider", () => {
15+
const createMockError = () => Object.assign(new Error(), { $metadata: {} });
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
});
20+
421
it("should return false when the provided error is falsy", () => {
522
expect(defaultRetryDecider(null as any)).toBe(false);
23+
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(0);
24+
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0);
25+
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
626
});
727

8-
it("should return true if the error was tagged as a connection error", () => {
9-
const err: Error & { connectionError?: boolean } = new Error();
10-
err.connectionError = true;
11-
expect(defaultRetryDecider(err)).toBe(true);
28+
it("should return true for ClockSkewError", () => {
29+
(isClockSkewError as jest.Mock).mockReturnValueOnce(true);
30+
expect(defaultRetryDecider(createMockError())).toBe(true);
31+
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
32+
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0);
33+
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
1234
});
1335

14-
for (const httpStatusCode of [429, 500, 502, 503, 504, 509]) {
15-
it(`should return true if the error represents a service response with an HTTP status code of ${httpStatusCode}`, () => {
16-
const err: any = new Error();
17-
err.$metadata = { httpStatusCode };
18-
expect(defaultRetryDecider(err)).toBe(true);
19-
});
20-
}
21-
22-
it('should return true if the response represents a "still processing" error', () => {
23-
const err = new Error();
24-
err.name = "PriorRequestNotComplete";
25-
expect(defaultRetryDecider(err)).toBe(true);
36+
it("should return true for ThrottlingError", () => {
37+
(isThrottlingError as jest.Mock).mockReturnValueOnce(true);
38+
expect(defaultRetryDecider(createMockError())).toBe(true);
39+
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
40+
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1);
41+
expect((isTransientError as jest.Mock).mock.calls.length).toBe(0);
2642
});
2743

28-
it("should return true if the response represents a throttling error", () => {
29-
const err = new Error();
30-
err.name = "TooManyRequestsException";
31-
expect(defaultRetryDecider(err)).toBe(true);
44+
it("should return true for TransientError", () => {
45+
(isTransientError as jest.Mock).mockReturnValueOnce(true);
46+
expect(defaultRetryDecider(createMockError())).toBe(true);
47+
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
48+
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1);
49+
expect((isTransientError as jest.Mock).mock.calls.length).toBe(1);
3250
});
3351

3452
it("should return false for other errors", () => {
35-
expect(defaultRetryDecider(new Error())).toBe(false);
53+
expect(defaultRetryDecider(createMockError())).toBe(false);
54+
expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1);
55+
expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1);
56+
expect((isTransientError as jest.Mock).mock.calls.length).toBe(1);
3657
});
3758
});

Diff for: packages/middleware-retry/src/retryDecider.ts

+8-20
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
1-
import { RETRYABLE_STATUS_CODES } from "./constants";
21
import {
32
isClockSkewError,
4-
isThrottlingError
3+
isThrottlingError,
4+
isTransientError
55
} from "@aws-sdk/service-error-classification";
6-
import { MetadataBearer, SdkError } from "@aws-sdk/types";
6+
import { SdkError } from "@aws-sdk/types";
77

88
export const defaultRetryDecider = (error: SdkError) => {
99
if (!error) {
1010
return false;
1111
}
1212

13-
if (error.connectionError) {
14-
return true;
15-
}
16-
17-
if (
18-
hasMetadata(error) &&
19-
error.$metadata.httpStatusCode &&
20-
RETRYABLE_STATUS_CODES.has(error.$metadata.httpStatusCode)
21-
) {
22-
return true;
23-
}
24-
25-
return isThrottlingError(error) || isClockSkewError(error);
13+
return (
14+
isClockSkewError(error) ||
15+
isThrottlingError(error) ||
16+
isTransientError(error)
17+
);
2618
};
27-
28-
function hasMetadata(error: any): error is MetadataBearer {
29-
return error?.$metadata;
30-
}

Diff for: packages/service-error-classification/package.json

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"url": "https://aws.amazon.com/javascript/"
1414
},
1515
"license": "Apache-2.0",
16+
"dependencies": {
17+
"@aws-sdk/types": "1.0.0-gamma.1"
18+
},
1619
"devDependencies": {
1720
"@types/jest": "^25.1.4",
1821
"jest": "^25.1.0",

Diff for: packages/service-error-classification/src/constants.ts

+15
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,18 @@ export const THROTTLING_ERROR_CODES = [
3535
"PriorRequestNotComplete",
3636
"EC2ThrottledException"
3737
];
38+
39+
/**
40+
* Error codes that indicate transient issues
41+
*/
42+
export const TRANSIENT_ERROR_CODES = [
43+
"AbortError",
44+
"TimeoutError",
45+
"RequestTimeout",
46+
"RequestTimeoutException"
47+
];
48+
49+
/**
50+
* Error codes that indicate transient issues
51+
*/
52+
export const TRANSIENT_ERROR_STATUS_CODES = [500, 502, 503, 504];

Diff for: packages/service-error-classification/src/index.spec.ts

+53-10
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
1-
import { CLOCK_SKEW_ERROR_CODES, THROTTLING_ERROR_CODES } from "./constants";
2-
import { isClockSkewError, isThrottlingError } from "./index";
1+
import {
2+
CLOCK_SKEW_ERROR_CODES,
3+
THROTTLING_ERROR_CODES,
4+
TRANSIENT_ERROR_CODES,
5+
TRANSIENT_ERROR_STATUS_CODES
6+
} from "./constants";
7+
import { isClockSkewError, isThrottlingError, isTransientError } from "./index";
8+
import { SdkError } from "@aws-sdk/types";
39

410
const checkForErrorType = (
5-
isErrorTypeFunc: (error: Error) => boolean,
6-
errorName: string,
11+
isErrorTypeFunc: (error: SdkError) => boolean,
12+
options: { name?: string; httpStatusCode?: number },
713
errorTypeResult: boolean
814
) => {
9-
const error = new Error();
10-
error.name = errorName;
15+
const { name, httpStatusCode } = options;
16+
const error = Object.assign(new Error(), {
17+
name,
18+
$metadata: { httpStatusCode }
19+
});
1120
expect(isErrorTypeFunc(error)).toBe(errorTypeResult);
1221
};
1322

1423
describe("isClockSkewError", () => {
1524
CLOCK_SKEW_ERROR_CODES.forEach(name => {
1625
it(`should declare error with the name "${name}" to be a ClockSkew error`, () => {
17-
checkForErrorType(isClockSkewError, name, true);
26+
checkForErrorType(isClockSkewError, { name }, true);
1827
});
1928
});
2029

2130
while (true) {
2231
const name = Math.random().toString(36).substring(2);
2332
if (!CLOCK_SKEW_ERROR_CODES.includes(name)) {
2433
it(`should not declare error with the name "${name}" to be a ClockSkew error`, () => {
25-
checkForErrorType(isClockSkewError, name, false);
34+
checkForErrorType(isClockSkewError, { name }, false);
2635
});
2736
break;
2837
}
@@ -32,15 +41,49 @@ describe("isClockSkewError", () => {
3241
describe("isThrottlingError", () => {
3342
THROTTLING_ERROR_CODES.forEach(name => {
3443
it(`should declare error with the name "${name}" to be a Throttling error`, () => {
35-
checkForErrorType(isThrottlingError, name, true);
44+
checkForErrorType(isThrottlingError, { name }, true);
3645
});
3746
});
3847

3948
while (true) {
4049
const name = Math.random().toString(36).substring(2);
4150
if (!THROTTLING_ERROR_CODES.includes(name)) {
4251
it(`should not declare error with the name "${name}" to be a Throttling error`, () => {
43-
checkForErrorType(isThrottlingError, name, false);
52+
checkForErrorType(isThrottlingError, { name }, false);
53+
});
54+
break;
55+
}
56+
}
57+
});
58+
59+
describe("isTransientError", () => {
60+
TRANSIENT_ERROR_CODES.forEach(name => {
61+
it(`should declare error with the name "${name}" to be a Throttling error`, () => {
62+
checkForErrorType(isTransientError, { name }, true);
63+
});
64+
});
65+
66+
TRANSIENT_ERROR_STATUS_CODES.forEach(httpStatusCode => {
67+
it(`should declare error with the HTTP Status Code "${httpStatusCode}" to be a Throttling error`, () => {
68+
checkForErrorType(isTransientError, { httpStatusCode }, true);
69+
});
70+
});
71+
72+
while (true) {
73+
const name = Math.random().toString(36).substring(2);
74+
if (!TRANSIENT_ERROR_CODES.includes(name)) {
75+
it(`should not declare error with the name "${name}" to be a Throttling error`, () => {
76+
checkForErrorType(isTransientError, { name }, false);
77+
});
78+
break;
79+
}
80+
}
81+
82+
while (true) {
83+
const httpStatusCode = Math.ceil(Math.random() * 10 ** 3);
84+
if (!TRANSIENT_ERROR_STATUS_CODES.includes(httpStatusCode)) {
85+
it(`should declare error with the HTTP Status Code "${httpStatusCode}" to be a Throttling error`, () => {
86+
checkForErrorType(isTransientError, { httpStatusCode }, false);
4487
});
4588
break;
4689
}

Diff for: packages/service-error-classification/src/index.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import { CLOCK_SKEW_ERROR_CODES, THROTTLING_ERROR_CODES } from "./constants";
1+
import {
2+
CLOCK_SKEW_ERROR_CODES,
3+
THROTTLING_ERROR_CODES,
4+
TRANSIENT_ERROR_CODES,
5+
TRANSIENT_ERROR_STATUS_CODES
6+
} from "./constants";
7+
import { SdkError } from "@aws-sdk/types";
28

3-
export const isClockSkewError = (error: Error) =>
9+
export const isClockSkewError = (error: SdkError) =>
410
CLOCK_SKEW_ERROR_CODES.includes(error.name);
511

6-
export const isThrottlingError = (error: Error) =>
12+
export const isThrottlingError = (error: SdkError) =>
713
THROTTLING_ERROR_CODES.includes(error.name);
14+
15+
export const isTransientError = (error: SdkError) =>
16+
TRANSIENT_ERROR_CODES.includes(error.name) ||
17+
TRANSIENT_ERROR_STATUS_CODES.includes(error.$metadata?.httpStatusCode || 0);

Diff for: packages/types/src/util.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export interface BodyLengthCalculator {
5454
}
5555

5656
// TODO Unify with the types created for the error parsers
57-
export type SdkError = Error & { connectionError?: boolean };
57+
export type SdkError = Error & MetadataBearer;
5858

5959
/**
6060
* Interface that specifies the retry behavior

0 commit comments

Comments
 (0)