diff --git a/packages/middleware-retry/package.json b/packages/middleware-retry/package.json index f85da2b149e6..663972a363ec 100644 --- a/packages/middleware-retry/package.json +++ b/packages/middleware-retry/package.json @@ -20,6 +20,7 @@ }, "devDependencies": { "@aws-sdk/protocol-http": "1.0.0-gamma.1", + "@aws-sdk/smithy-client": "1.0.0-gamma.1", "@types/jest": "^25.1.4", "jest": "^25.1.0", "typescript": "~3.8.3" diff --git a/packages/middleware-retry/src/defaultStrategy.ts b/packages/middleware-retry/src/defaultStrategy.ts index bef7e1479a20..bb7d41a21960 100644 --- a/packages/middleware-retry/src/defaultStrategy.ts +++ b/packages/middleware-retry/src/defaultStrategy.ts @@ -5,8 +5,8 @@ import { import { defaultDelayDecider } from "./delayDecider"; import { defaultRetryDecider } from "./retryDecider"; import { isThrottlingError } from "@aws-sdk/service-error-classification"; +import { SdkError } from "@aws-sdk/smithy-client"; import { - SdkError, FinalizeHandler, MetadataBearer, FinalizeHandlerArguments, diff --git a/packages/middleware-retry/src/index.spec.ts b/packages/middleware-retry/src/index.spec.ts index b21b37aaa879..f3863aec5987 100644 --- a/packages/middleware-retry/src/index.spec.ts +++ b/packages/middleware-retry/src/index.spec.ts @@ -7,7 +7,7 @@ import { resolveRetryConfig } from "./configurations"; import * as delayDeciderModule from "./delayDecider"; import { ExponentialBackOffStrategy, RetryDecider } from "./defaultStrategy"; import { HttpRequest } from "@aws-sdk/protocol-http"; -import { SdkError } from "@aws-sdk/types"; +import { SdkError } from "@aws-sdk/smithy-client"; describe("retryMiddleware", () => { it("should not retry when the handler completes successfully", async () => { diff --git a/packages/middleware-retry/src/retryDecider.spec.ts b/packages/middleware-retry/src/retryDecider.spec.ts index 9ff61d463426..15ed0732cfee 100644 --- a/packages/middleware-retry/src/retryDecider.spec.ts +++ b/packages/middleware-retry/src/retryDecider.spec.ts @@ -1,18 +1,22 @@ import { + isRetryableByTrait, isClockSkewError, isThrottlingError, isTransientError } from "@aws-sdk/service-error-classification"; import { defaultRetryDecider } from "./retryDecider"; +import { SdkError } from "@aws-sdk/smithy-client"; jest.mock("@aws-sdk/service-error-classification", () => ({ + isRetryableByTrait: jest.fn().mockReturnValue(false), isClockSkewError: jest.fn().mockReturnValue(false), isThrottlingError: jest.fn().mockReturnValue(false), isTransientError: jest.fn().mockReturnValue(false) })); describe("defaultRetryDecider", () => { - const createMockError = () => Object.assign(new Error(), { $metadata: {} }); + const createMockError = () => + Object.assign(new Error(), { $metadata: {} }) as SdkError; beforeEach(() => { jest.clearAllMocks(); @@ -20,6 +24,16 @@ describe("defaultRetryDecider", () => { it("should return false when the provided error is falsy", () => { expect(defaultRetryDecider(null as any)).toBe(false); + expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(0); + expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(0); + expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0); + expect((isTransientError as jest.Mock).mock.calls.length).toBe(0); + }); + + it("should return true for RetryableByTrait error", () => { + (isRetryableByTrait as jest.Mock).mockReturnValueOnce(true); + expect(defaultRetryDecider(createMockError())).toBe(true); + expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1); expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(0); expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0); expect((isTransientError as jest.Mock).mock.calls.length).toBe(0); @@ -28,6 +42,7 @@ describe("defaultRetryDecider", () => { it("should return true for ClockSkewError", () => { (isClockSkewError as jest.Mock).mockReturnValueOnce(true); expect(defaultRetryDecider(createMockError())).toBe(true); + expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1); expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1); expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(0); expect((isTransientError as jest.Mock).mock.calls.length).toBe(0); @@ -36,6 +51,7 @@ describe("defaultRetryDecider", () => { it("should return true for ThrottlingError", () => { (isThrottlingError as jest.Mock).mockReturnValueOnce(true); expect(defaultRetryDecider(createMockError())).toBe(true); + expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1); expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1); expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1); expect((isTransientError as jest.Mock).mock.calls.length).toBe(0); @@ -44,6 +60,7 @@ describe("defaultRetryDecider", () => { it("should return true for TransientError", () => { (isTransientError as jest.Mock).mockReturnValueOnce(true); expect(defaultRetryDecider(createMockError())).toBe(true); + expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1); expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1); expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1); expect((isTransientError as jest.Mock).mock.calls.length).toBe(1); @@ -51,6 +68,7 @@ describe("defaultRetryDecider", () => { it("should return false for other errors", () => { expect(defaultRetryDecider(createMockError())).toBe(false); + expect((isRetryableByTrait as jest.Mock).mock.calls.length).toBe(1); expect((isClockSkewError as jest.Mock).mock.calls.length).toBe(1); expect((isThrottlingError as jest.Mock).mock.calls.length).toBe(1); expect((isTransientError as jest.Mock).mock.calls.length).toBe(1); diff --git a/packages/middleware-retry/src/retryDecider.ts b/packages/middleware-retry/src/retryDecider.ts index 7a7e0b32ad36..83a3fec2bd7c 100644 --- a/packages/middleware-retry/src/retryDecider.ts +++ b/packages/middleware-retry/src/retryDecider.ts @@ -1,9 +1,10 @@ import { isClockSkewError, + isRetryableByTrait, isThrottlingError, isTransientError } from "@aws-sdk/service-error-classification"; -import { SdkError } from "@aws-sdk/types"; +import { SdkError } from "@aws-sdk/smithy-client"; export const defaultRetryDecider = (error: SdkError) => { if (!error) { @@ -11,6 +12,7 @@ export const defaultRetryDecider = (error: SdkError) => { } return ( + isRetryableByTrait(error) || isClockSkewError(error) || isThrottlingError(error) || isTransientError(error) diff --git a/packages/service-error-classification/package.json b/packages/service-error-classification/package.json index 555178ebb01d..56623b99acc2 100644 --- a/packages/service-error-classification/package.json +++ b/packages/service-error-classification/package.json @@ -13,10 +13,8 @@ "url": "https://aws.amazon.com/javascript/" }, "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "1.0.0-gamma.1" - }, "devDependencies": { + "@aws-sdk/smithy-client": "1.0.0-gamma.1", "@types/jest": "^25.1.4", "jest": "^25.1.0", "typescript": "~3.8.3" diff --git a/packages/service-error-classification/src/index.spec.ts b/packages/service-error-classification/src/index.spec.ts index a7c6ac627078..2ab1da02f3b6 100644 --- a/packages/service-error-classification/src/index.spec.ts +++ b/packages/service-error-classification/src/index.spec.ts @@ -4,22 +4,43 @@ import { TRANSIENT_ERROR_CODES, TRANSIENT_ERROR_STATUS_CODES } from "./constants"; -import { isClockSkewError, isThrottlingError, isTransientError } from "./index"; -import { SdkError } from "@aws-sdk/types"; +import { + isRetryableByTrait, + isClockSkewError, + isThrottlingError, + isTransientError +} from "./index"; +import { SdkError, RetryableTrait } from "@aws-sdk/smithy-client"; const checkForErrorType = ( isErrorTypeFunc: (error: SdkError) => boolean, - options: { name?: string; httpStatusCode?: number }, + options: { + name?: string; + httpStatusCode?: number; + $retryable?: RetryableTrait; + }, errorTypeResult: boolean ) => { - const { name, httpStatusCode } = options; + const { name, httpStatusCode, $retryable } = options; const error = Object.assign(new Error(), { name, - $metadata: { httpStatusCode } + $metadata: { httpStatusCode }, + $retryable }); - expect(isErrorTypeFunc(error)).toBe(errorTypeResult); + expect(isErrorTypeFunc(error as SdkError)).toBe(errorTypeResult); }; +describe("isRetryableByTrait", () => { + it("should declare error with $retryable set to be a Retryable by trait", () => { + const $retryable = {}; + checkForErrorType(isRetryableByTrait, { $retryable }, true); + }); + + it("should not declare error with $retryable not set to be a Retryable by trait", () => { + checkForErrorType(isRetryableByTrait, {}, false); + }); +}); + describe("isClockSkewError", () => { CLOCK_SKEW_ERROR_CODES.forEach(name => { it(`should declare error with the name "${name}" to be a ClockSkew error`, () => { @@ -54,6 +75,21 @@ describe("isThrottlingError", () => { break; } } + + it("should declare error with $retryable.throttling set to true to be a Throttling error", () => { + const $retryable = { throttling: true }; + checkForErrorType(isThrottlingError, { $retryable }, true); + }); + + it("should not declare error with $retryable.throttling set to false to be a Throttling error", () => { + const $retryable = { throttling: false }; + checkForErrorType(isThrottlingError, { $retryable }, false); + }); + + it("should not declare error with $retryable.throttling not set to be a Throttling error", () => { + const $retryable = {}; + checkForErrorType(isThrottlingError, { $retryable }, false); + }); }); describe("isTransientError", () => { diff --git a/packages/service-error-classification/src/index.ts b/packages/service-error-classification/src/index.ts index 04581cf5d7b0..f046b22d118d 100644 --- a/packages/service-error-classification/src/index.ts +++ b/packages/service-error-classification/src/index.ts @@ -4,13 +4,17 @@ import { TRANSIENT_ERROR_CODES, TRANSIENT_ERROR_STATUS_CODES } from "./constants"; -import { SdkError } from "@aws-sdk/types"; +import { SdkError } from "@aws-sdk/smithy-client"; + +export const isRetryableByTrait = (error: SdkError) => + error.$retryable !== undefined; export const isClockSkewError = (error: SdkError) => CLOCK_SKEW_ERROR_CODES.includes(error.name); export const isThrottlingError = (error: SdkError) => - THROTTLING_ERROR_CODES.includes(error.name); + THROTTLING_ERROR_CODES.includes(error.name) || + error.$retryable?.throttling == true; export const isTransientError = (error: SdkError) => TRANSIENT_ERROR_CODES.includes(error.name) || diff --git a/packages/smithy-client/src/exception.ts b/packages/smithy-client/src/exception.ts index 0e1fdccfaf4b..54aca7d77e2d 100644 --- a/packages/smithy-client/src/exception.ts +++ b/packages/smithy-client/src/exception.ts @@ -1,3 +1,5 @@ +import { RetryableTrait } from "./retryable-trait"; + /** * Type that is implemented by all Smithy shapes marked with the * error trait. @@ -17,4 +19,9 @@ export interface SmithyException { * The service that encountered the exception. */ readonly $service?: string; + + /** + * Indicates that an error MAY be retried by the client. + */ + readonly $retryable?: RetryableTrait; } diff --git a/packages/smithy-client/src/index.ts b/packages/smithy-client/src/index.ts index 5c6b196fae9f..c3c1de955ce4 100644 --- a/packages/smithy-client/src/index.ts +++ b/packages/smithy-client/src/index.ts @@ -10,3 +10,5 @@ export * from "./lazy-json"; export * from "./date-utils"; export * from "./split-every"; export * from "./constants"; +export * from "./retryable-trait"; +export * from "./sdk-error"; diff --git a/packages/smithy-client/src/retryable-trait.ts b/packages/smithy-client/src/retryable-trait.ts new file mode 100644 index 000000000000..6bbd5b054313 --- /dev/null +++ b/packages/smithy-client/src/retryable-trait.ts @@ -0,0 +1,10 @@ +/** + * A structure shape with the error trait. + * https://awslabs.github.io/smithy/spec/core.html#retryable-trait + */ +export interface RetryableTrait { + /** + * Indicates that the error is a retryable throttling error. + */ + readonly throttling?: boolean; +} diff --git a/packages/smithy-client/src/sdk-error.ts b/packages/smithy-client/src/sdk-error.ts new file mode 100644 index 000000000000..18256727b737 --- /dev/null +++ b/packages/smithy-client/src/sdk-error.ts @@ -0,0 +1,4 @@ +import { SmithyException } from "./exception"; +import { MetadataBearer } from "@aws-sdk/types"; + +export type SdkError = Error & SmithyException & MetadataBearer; diff --git a/packages/types/src/util.ts b/packages/types/src/util.ts index 424ed53f42e9..92083efb2973 100644 --- a/packages/types/src/util.ts +++ b/packages/types/src/util.ts @@ -53,9 +53,6 @@ export interface BodyLengthCalculator { (body: any): number | undefined; } -// TODO Unify with the types created for the error parsers -export type SdkError = Error & MetadataBearer; - /** * Interface that specifies the retry behavior */