Skip to content

Commit 0e1c9ca

Browse files
committed
fix(service-error-classification): consider $retryable trait errors to be transient
1 parent 969d21e commit 0e1c9ca

File tree

10 files changed

+453
-59
lines changed

10 files changed

+453
-59
lines changed

.changeset/mean-paws-check.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/service-error-classification": patch
3+
"@smithy/util-retry": patch
4+
---
5+
6+
make $retryable-trait errors considered transient in StandardRetryStrategyV2

packages/service-error-classification/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const isThrottlingError = (error: SdkError) =>
5555
* the name "TimeoutError" to be checked by the TRANSIENT_ERROR_CODES condition.
5656
*/
5757
export const isTransientError = (error: SdkError, depth = 0): boolean =>
58+
isRetryableByTrait(error) ||
5859
isClockSkewCorrectedError(error) ||
5960
TRANSIENT_ERROR_CODES.includes(error.name) ||
6061
NODEJS_TIMEOUT_ERROR_CODES.includes((error as { code?: string })?.code || "") ||

packages/util-retry/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
"format": "prettier --config ../../prettier.config.js --ignore-path ../../.prettierignore --write \"**/*.{ts,md,json}\"",
1717
"extract:docs": "api-extractor run --local",
1818
"test": "yarn g:vitest run",
19-
"test:watch": "yarn g:vitest watch"
19+
"test:watch": "yarn g:vitest watch",
20+
"test:integration": "yarn g:vitest run -c vitest.config.integ.mts",
21+
"test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.mts"
2022
},
2123
"keywords": [
2224
"aws",
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { cbor } from "@smithy/core/cbor";
2+
import { HttpResponse } from "@smithy/protocol-http";
3+
import { requireRequestsFrom } from "@smithy/util-test/src";
4+
import { Readable } from "node:stream";
5+
import { describe, expect, test as it } from "vitest";
6+
import { XYZService } from "xyz";
7+
8+
describe("retries", () => {
9+
function createCborResponse(body: any, status = 200) {
10+
const bytes = cbor.serialize(body);
11+
return new HttpResponse({
12+
headers: {
13+
"smithy-protocol": "rpc-v2-cbor",
14+
},
15+
body: Readable.from(bytes),
16+
statusCode: status,
17+
});
18+
}
19+
20+
it("should retry throttling and transient-error status codes", async () => {
21+
const client = new XYZService({
22+
endpoint: "https://localhost/nowhere",
23+
});
24+
25+
requireRequestsFrom(client)
26+
.toMatch({
27+
hostname: /localhost/,
28+
})
29+
.respondWith(
30+
createCborResponse(
31+
{
32+
__type: "HaltError",
33+
},
34+
429
35+
),
36+
createCborResponse(
37+
{
38+
__type: "HaltError",
39+
},
40+
500
41+
),
42+
createCborResponse("", 200)
43+
);
44+
45+
const response = await client.getNumbers().catch((e) => e);
46+
47+
expect(response.$metadata.attempts).toEqual(3);
48+
});
49+
50+
it("should retry when a retryable trait is modeled", async () => {
51+
const client = new XYZService({
52+
endpoint: "https://localhost/nowhere",
53+
});
54+
55+
requireRequestsFrom(client)
56+
.toMatch({
57+
hostname: /localhost/,
58+
})
59+
.respondWith(
60+
createCborResponse(
61+
{
62+
__type: "RetryableError",
63+
},
64+
400 // not retryable status code
65+
),
66+
createCborResponse(
67+
{
68+
__type: "RetryableError",
69+
},
70+
400 // not retryable status code
71+
),
72+
createCborResponse("", 200)
73+
);
74+
75+
const response = await client.getNumbers().catch((e) => e);
76+
77+
expect(response.$metadata.attempts).toEqual(3);
78+
});
79+
80+
it("should retry retryable trait with throttling", async () => {
81+
const client = new XYZService({
82+
endpoint: "https://localhost/nowhere",
83+
});
84+
85+
requireRequestsFrom(client)
86+
.toMatch({
87+
hostname: /localhost/,
88+
})
89+
.respondWith(
90+
createCborResponse(
91+
{
92+
__type: "CodedThrottlingError",
93+
},
94+
429
95+
),
96+
createCborResponse(
97+
{
98+
__type: "MysteryThrottlingError",
99+
},
100+
400 // not a retryable status code, but error is modeled as retryable.
101+
),
102+
createCborResponse("", 200)
103+
);
104+
105+
const response = await client.getNumbers().catch((e) => e);
106+
107+
expect(response.$metadata.attempts).toEqual(3);
108+
});
109+
110+
it("should not retry if the error is not modeled with retryable trait and is not otherwise retryable", async () => {
111+
const client = new XYZService({
112+
endpoint: "https://localhost/nowhere",
113+
});
114+
115+
requireRequestsFrom(client)
116+
.toMatch({
117+
hostname: /localhost/,
118+
})
119+
.respondWith(
120+
createCborResponse(
121+
{
122+
__type: "HaltError",
123+
},
124+
429 // not modeled as retryable, but this is a retryable status code.
125+
),
126+
createCborResponse(
127+
{
128+
__type: "HaltError",
129+
},
130+
400
131+
),
132+
createCborResponse("", 200)
133+
);
134+
135+
const response = await client.getNumbers().catch((e) => e);
136+
137+
// stopped at the second error.
138+
expect(response.$metadata.attempts).toEqual(2);
139+
});
140+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
include: ["**/*.integ.spec.ts"],
6+
environment: "node",
7+
},
8+
});

private/my-local-model/src/commands/GetNumbersCommand.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export interface GetNumbersCommandOutput extends GetNumbersResponse, __MetadataB
5656
* @see {@link GetNumbersCommandOutput} for command's `response` shape.
5757
* @see {@link XYZServiceClientResolvedConfig | config} for XYZServiceClient's `config` shape.
5858
*
59+
* @throws {@link CodedThrottlingError} (client fault)
60+
*
61+
* @throws {@link MysteryThrottlingError} (client fault)
62+
*
63+
* @throws {@link RetryableError} (client fault)
64+
*
65+
* @throws {@link HaltError} (client fault)
66+
*
5967
* @throws {@link XYZServiceServiceException}
6068
* <p>Base exception class for all service exceptions from XYZService service.</p>
6169
*

private/my-local-model/src/models/models_0.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
// smithy-typescript generated code
2+
import { XYZServiceServiceException as __BaseException } from "./XYZServiceServiceException";
23
import { NumericValue } from "@smithy/core/serde";
4+
import { ExceptionOptionType as __ExceptionOptionType } from "@smithy/smithy-client";
5+
6+
/**
7+
* @public
8+
*/
9+
export class CodedThrottlingError extends __BaseException {
10+
readonly name: "CodedThrottlingError" = "CodedThrottlingError";
11+
readonly $fault: "client" = "client";
12+
$retryable = {
13+
throttling: true,
14+
};
15+
/**
16+
* @internal
17+
*/
18+
constructor(opts: __ExceptionOptionType<CodedThrottlingError, __BaseException>) {
19+
super({
20+
name: "CodedThrottlingError",
21+
$fault: "client",
22+
...opts,
23+
});
24+
Object.setPrototypeOf(this, CodedThrottlingError.prototype);
25+
}
26+
}
327

428
/**
529
* @public
@@ -16,3 +40,64 @@ export interface GetNumbersResponse {
1640
bigDecimal?: NumericValue | undefined;
1741
bigInteger?: bigint | undefined;
1842
}
43+
44+
/**
45+
* @public
46+
*/
47+
export class HaltError extends __BaseException {
48+
readonly name: "HaltError" = "HaltError";
49+
readonly $fault: "client" = "client";
50+
/**
51+
* @internal
52+
*/
53+
constructor(opts: __ExceptionOptionType<HaltError, __BaseException>) {
54+
super({
55+
name: "HaltError",
56+
$fault: "client",
57+
...opts,
58+
});
59+
Object.setPrototypeOf(this, HaltError.prototype);
60+
}
61+
}
62+
63+
/**
64+
* @public
65+
*/
66+
export class MysteryThrottlingError extends __BaseException {
67+
readonly name: "MysteryThrottlingError" = "MysteryThrottlingError";
68+
readonly $fault: "client" = "client";
69+
$retryable = {
70+
throttling: true,
71+
};
72+
/**
73+
* @internal
74+
*/
75+
constructor(opts: __ExceptionOptionType<MysteryThrottlingError, __BaseException>) {
76+
super({
77+
name: "MysteryThrottlingError",
78+
$fault: "client",
79+
...opts,
80+
});
81+
Object.setPrototypeOf(this, MysteryThrottlingError.prototype);
82+
}
83+
}
84+
85+
/**
86+
* @public
87+
*/
88+
export class RetryableError extends __BaseException {
89+
readonly name: "RetryableError" = "RetryableError";
90+
readonly $fault: "client" = "client";
91+
$retryable = {};
92+
/**
93+
* @internal
94+
*/
95+
constructor(opts: __ExceptionOptionType<RetryableError, __BaseException>) {
96+
super({
97+
name: "RetryableError",
98+
$fault: "client",
99+
...opts,
100+
});
101+
Object.setPrototypeOf(this, RetryableError.prototype);
102+
}
103+
}

0 commit comments

Comments
 (0)