diff --git a/.changeset/mean-paws-check.md b/.changeset/mean-paws-check.md
new file mode 100644
index 00000000000..59c49d6a4f8
--- /dev/null
+++ b/.changeset/mean-paws-check.md
@@ -0,0 +1,6 @@
+---
+"@smithy/service-error-classification": patch
+"@smithy/util-retry": patch
+---
+
+make $retryable-trait errors considered transient in StandardRetryStrategyV2
diff --git a/packages/middleware-apply-body-checksum/src/middleware-apply-body-checksum.integ.spec.ts b/packages/middleware-apply-body-checksum/src/middleware-apply-body-checksum.integ.spec.ts
index 449049e4894..9bdcf5edd44 100644
--- a/packages/middleware-apply-body-checksum/src/middleware-apply-body-checksum.integ.spec.ts
+++ b/packages/middleware-apply-body-checksum/src/middleware-apply-body-checksum.integ.spec.ts
@@ -6,7 +6,14 @@ import { requireRequestsFrom } from "../../../private/util-test/src/index";
describe("middleware-apply-body-checksum", () => {
describe(Weather.name, () => {
it("should add body-checksum", async () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
headers: {
"content-md5": /^.{22}(==)?$/i,
diff --git a/packages/middleware-content-length/src/middleware-content-length.integ.spec.ts b/packages/middleware-content-length/src/middleware-content-length.integ.spec.ts
index f5ccc2b0bf7..487e5abda49 100644
--- a/packages/middleware-content-length/src/middleware-content-length.integ.spec.ts
+++ b/packages/middleware-content-length/src/middleware-content-length.integ.spec.ts
@@ -6,7 +6,14 @@ import { requireRequestsFrom } from "../../../private/util-test/src/index";
describe("middleware-content-length", () => {
describe(Weather.name, () => {
it("should not add content-length if no body", async () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
headers: {
"content-length": /undefined/,
@@ -24,7 +31,14 @@ describe("middleware-content-length", () => {
// This tests that content-length gets set to `2`, only where bodies are
// sent in the request.
it("should add content-length if body present", async () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
headers: {
"content-length": /2/,
diff --git a/packages/middleware-retry/src/middleware-retry.integ.spec.ts b/packages/middleware-retry/src/middleware-retry.integ.spec.ts
index f2c0d368e61..d22a529ca62 100644
--- a/packages/middleware-retry/src/middleware-retry.integ.spec.ts
+++ b/packages/middleware-retry/src/middleware-retry.integ.spec.ts
@@ -6,7 +6,14 @@ import { requireRequestsFrom } from "../../../private/util-test/src/index";
describe("middleware-retry", () => {
describe(Weather.name, () => {
it("should set retry headers", async () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
hostname: "foo.bar",
diff --git a/packages/middleware-serde/src/middleware-serde.integ.spec.ts b/packages/middleware-serde/src/middleware-serde.integ.spec.ts
index 5e60de61afb..06549185864 100644
--- a/packages/middleware-serde/src/middleware-serde.integ.spec.ts
+++ b/packages/middleware-serde/src/middleware-serde.integ.spec.ts
@@ -6,7 +6,14 @@ import { requireRequestsFrom } from "../../../private/util-test/src/index";
describe("middleware-serde", () => {
describe(Weather.name, () => {
it("should serialize TestProtocol", async () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
method: "PUT",
hostname: "foo.bar",
diff --git a/packages/service-error-classification/src/index.ts b/packages/service-error-classification/src/index.ts
index d4a98484c21..3d286509342 100644
--- a/packages/service-error-classification/src/index.ts
+++ b/packages/service-error-classification/src/index.ts
@@ -9,7 +9,7 @@ import {
TRANSIENT_ERROR_STATUS_CODES,
} from "./constants";
-export const isRetryableByTrait = (error: SdkError) => error.$retryable !== undefined;
+export const isRetryableByTrait = (error: SdkError) => error?.$retryable !== undefined;
/**
* @deprecated use isClockSkewCorrectedError. This is only used in deprecated code.
@@ -55,6 +55,7 @@ export const isThrottlingError = (error: SdkError) =>
* the name "TimeoutError" to be checked by the TRANSIENT_ERROR_CODES condition.
*/
export const isTransientError = (error: SdkError, depth = 0): boolean =>
+ isRetryableByTrait(error) ||
isClockSkewCorrectedError(error) ||
TRANSIENT_ERROR_CODES.includes(error.name) ||
NODEJS_TIMEOUT_ERROR_CODES.includes((error as { code?: string })?.code || "") ||
diff --git a/packages/util-retry/package.json b/packages/util-retry/package.json
index 16ef9b665f0..756e67c20dd 100644
--- a/packages/util-retry/package.json
+++ b/packages/util-retry/package.json
@@ -16,7 +16,9 @@
"format": "prettier --config ../../prettier.config.js --ignore-path ../../.prettierignore --write \"**/*.{ts,md,json}\"",
"extract:docs": "api-extractor run --local",
"test": "yarn g:vitest run",
- "test:watch": "yarn g:vitest watch"
+ "test:watch": "yarn g:vitest watch",
+ "test:integration": "yarn g:vitest run -c vitest.config.integ.mts",
+ "test:integration:watch": "yarn g:vitest watch -c vitest.config.integ.mts"
},
"keywords": [
"aws",
diff --git a/packages/util-retry/src/retries.integ.spec.ts b/packages/util-retry/src/retries.integ.spec.ts
new file mode 100644
index 00000000000..f5d37cef458
--- /dev/null
+++ b/packages/util-retry/src/retries.integ.spec.ts
@@ -0,0 +1,140 @@
+import { cbor } from "@smithy/core/cbor";
+import { HttpResponse } from "@smithy/protocol-http";
+import { requireRequestsFrom } from "@smithy/util-test/src";
+import { Readable } from "node:stream";
+import { describe, expect, test as it } from "vitest";
+import { XYZService } from "xyz";
+
+describe("retries", () => {
+ function createCborResponse(body: any, status = 200) {
+ const bytes = cbor.serialize(body);
+ return new HttpResponse({
+ headers: {
+ "smithy-protocol": "rpc-v2-cbor",
+ },
+ body: Readable.from(bytes),
+ statusCode: status,
+ });
+ }
+
+ it("should retry throttling and transient-error status codes", async () => {
+ const client = new XYZService({
+ endpoint: "https://localhost/nowhere",
+ });
+
+ requireRequestsFrom(client)
+ .toMatch({
+ hostname: /localhost/,
+ })
+ .respondWith(
+ createCborResponse(
+ {
+ __type: "HaltError",
+ },
+ 429
+ ),
+ createCborResponse(
+ {
+ __type: "HaltError",
+ },
+ 500
+ ),
+ createCborResponse("", 200)
+ );
+
+ const response = await client.getNumbers().catch((e) => e);
+
+ expect(response.$metadata.attempts).toEqual(3);
+ });
+
+ it("should retry when a retryable trait is modeled", async () => {
+ const client = new XYZService({
+ endpoint: "https://localhost/nowhere",
+ });
+
+ requireRequestsFrom(client)
+ .toMatch({
+ hostname: /localhost/,
+ })
+ .respondWith(
+ createCborResponse(
+ {
+ __type: "RetryableError",
+ },
+ 400 // not retryable status code
+ ),
+ createCborResponse(
+ {
+ __type: "RetryableError",
+ },
+ 400 // not retryable status code
+ ),
+ createCborResponse("", 200)
+ );
+
+ const response = await client.getNumbers().catch((e) => e);
+
+ expect(response.$metadata.attempts).toEqual(3);
+ });
+
+ it("should retry retryable trait with throttling", async () => {
+ const client = new XYZService({
+ endpoint: "https://localhost/nowhere",
+ });
+
+ requireRequestsFrom(client)
+ .toMatch({
+ hostname: /localhost/,
+ })
+ .respondWith(
+ createCborResponse(
+ {
+ __type: "CodedThrottlingError",
+ },
+ 429
+ ),
+ createCborResponse(
+ {
+ __type: "MysteryThrottlingError",
+ },
+ 400 // not a retryable status code, but error is modeled as retryable.
+ ),
+ createCborResponse("", 200)
+ );
+
+ const response = await client.getNumbers().catch((e) => e);
+
+ expect(response.$metadata.attempts).toEqual(3);
+ });
+
+ it("should not retry if the error is not modeled with retryable trait and is not otherwise retryable", async () => {
+ const client = new XYZService({
+ endpoint: "https://localhost/nowhere",
+ });
+
+ requireRequestsFrom(client)
+ .toMatch({
+ hostname: /localhost/,
+ })
+ .respondWith(
+ createCborResponse(
+ {
+ __type: "HaltError",
+ },
+ 429 // not modeled as retryable, but this is a retryable status code.
+ ),
+ createCborResponse(
+ {
+ __type: "HaltError",
+ },
+ 400
+ ),
+ createCborResponse("", 200)
+ );
+
+ const response = await client.getNumbers().catch((e) => e);
+
+ // stopped at the second error.
+ expect(response.$metadata.attempts).toEqual(2);
+ });
+});
diff --git a/packages/util-retry/vitest.config.integ.mts b/packages/util-retry/vitest.config.integ.mts
new file mode 100644
index 00000000000..5802db1ac64
--- /dev/null
+++ b/packages/util-retry/vitest.config.integ.mts
@@ -0,0 +1,8 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: ["**/*.integ.spec.ts"],
+ environment: "node",
+ },
+});
diff --git a/packages/util-stream/src/util-stream.integ.spec.ts b/packages/util-stream/src/util-stream.integ.spec.ts
index 12d460f2d3c..3cab355eefa 100644
--- a/packages/util-stream/src/util-stream.integ.spec.ts
+++ b/packages/util-stream/src/util-stream.integ.spec.ts
@@ -12,7 +12,14 @@ import { requireRequestsFrom } from "../../../private/util-test/src/index";
describe("util-stream", () => {
describe(Weather.name, () => {
it("should be uniform between string and Uint8Array payloads", async () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
method: "POST",
hostname: "foo.bar",
@@ -47,7 +54,14 @@ describe("util-stream", () => {
});
describe("blob helper integration", () => {
- const client = new Weather({ endpoint: "https://foo.bar" });
+ const client = new Weather({
+ endpoint: "https://foo.bar",
+ region: "us-west-2",
+ credentials: {
+ accessKeyId: "INTEG",
+ secretAccessKey: "INTEG",
+ },
+ });
requireRequestsFrom(client).toMatch({
method: "POST",
diff --git a/private/my-local-model/src/commands/GetNumbersCommand.ts b/private/my-local-model/src/commands/GetNumbersCommand.ts
index 38b4a54a9fa..356bcb5ad2e 100644
--- a/private/my-local-model/src/commands/GetNumbersCommand.ts
+++ b/private/my-local-model/src/commands/GetNumbersCommand.ts
@@ -56,6 +56,14 @@ export interface GetNumbersCommandOutput extends GetNumbersResponse, __MetadataB
* @see {@link GetNumbersCommandOutput} for command's `response` shape.
* @see {@link XYZServiceClientResolvedConfig | config} for XYZServiceClient's `config` shape.
*
+ * @throws {@link CodedThrottlingError} (client fault)
+ *
+ * @throws {@link MysteryThrottlingError} (client fault)
+ *
+ * @throws {@link RetryableError} (client fault)
+ *
+ * @throws {@link HaltError} (client fault)
+ *
* @throws {@link XYZServiceServiceException}
*
Base exception class for all service exceptions from XYZService service.
*
diff --git a/private/my-local-model/src/models/models_0.ts b/private/my-local-model/src/models/models_0.ts
index 5be0453f46e..9fb64af1dc7 100644
--- a/private/my-local-model/src/models/models_0.ts
+++ b/private/my-local-model/src/models/models_0.ts
@@ -1,5 +1,29 @@
// smithy-typescript generated code
+import { XYZServiceServiceException as __BaseException } from "./XYZServiceServiceException";
import { NumericValue } from "@smithy/core/serde";
+import { ExceptionOptionType as __ExceptionOptionType } from "@smithy/smithy-client";
+
+/**
+ * @public
+ */
+export class CodedThrottlingError extends __BaseException {
+ readonly name: "CodedThrottlingError" = "CodedThrottlingError";
+ readonly $fault: "client" = "client";
+ $retryable = {
+ throttling: true,
+ };
+ /**
+ * @internal
+ */
+ constructor(opts: __ExceptionOptionType) {
+ super({
+ name: "CodedThrottlingError",
+ $fault: "client",
+ ...opts,
+ });
+ Object.setPrototypeOf(this, CodedThrottlingError.prototype);
+ }
+}
/**
* @public
@@ -16,3 +40,64 @@ export interface GetNumbersResponse {
bigDecimal?: NumericValue | undefined;
bigInteger?: bigint | undefined;
}
+
+/**
+ * @public
+ */
+export class HaltError extends __BaseException {
+ readonly name: "HaltError" = "HaltError";
+ readonly $fault: "client" = "client";
+ /**
+ * @internal
+ */
+ constructor(opts: __ExceptionOptionType) {
+ super({
+ name: "HaltError",
+ $fault: "client",
+ ...opts,
+ });
+ Object.setPrototypeOf(this, HaltError.prototype);
+ }
+}
+
+/**
+ * @public
+ */
+export class MysteryThrottlingError extends __BaseException {
+ readonly name: "MysteryThrottlingError" = "MysteryThrottlingError";
+ readonly $fault: "client" = "client";
+ $retryable = {
+ throttling: true,
+ };
+ /**
+ * @internal
+ */
+ constructor(opts: __ExceptionOptionType) {
+ super({
+ name: "MysteryThrottlingError",
+ $fault: "client",
+ ...opts,
+ });
+ Object.setPrototypeOf(this, MysteryThrottlingError.prototype);
+ }
+}
+
+/**
+ * @public
+ */
+export class RetryableError extends __BaseException {
+ readonly name: "RetryableError" = "RetryableError";
+ readonly $fault: "client" = "client";
+ $retryable = {};
+ /**
+ * @internal
+ */
+ constructor(opts: __ExceptionOptionType) {
+ super({
+ name: "RetryableError",
+ $fault: "client",
+ ...opts,
+ });
+ Object.setPrototypeOf(this, RetryableError.prototype);
+ }
+}
diff --git a/private/my-local-model/src/protocols/Rpcv2cbor.ts b/private/my-local-model/src/protocols/Rpcv2cbor.ts
index eacb1536e54..70558c90ce8 100644
--- a/private/my-local-model/src/protocols/Rpcv2cbor.ts
+++ b/private/my-local-model/src/protocols/Rpcv2cbor.ts
@@ -1,7 +1,14 @@
// smithy-typescript generated code
import { GetNumbersCommandInput, GetNumbersCommandOutput } from "../commands/GetNumbersCommand";
import { XYZServiceServiceException as __BaseException } from "../models/XYZServiceServiceException";
-import { GetNumbersRequest, GetNumbersResponse } from "../models/models_0";
+import {
+ CodedThrottlingError,
+ GetNumbersRequest,
+ GetNumbersResponse,
+ HaltError,
+ MysteryThrottlingError,
+ RetryableError,
+} from "../models/models_0";
import {
buildHttpRpcRequest,
cbor,
@@ -12,7 +19,13 @@ import {
} from "@smithy/core/cbor";
import { nv as __nv } from "@smithy/core/serde";
import { HttpRequest as __HttpRequest, HttpResponse as __HttpResponse } from "@smithy/protocol-http";
-import { _json, collectBody, take, withBaseException } from "@smithy/smithy-client";
+import {
+ decorateServiceException as __decorateServiceException,
+ _json,
+ collectBody,
+ take,
+ withBaseException,
+} from "@smithy/smithy-client";
import {
Endpoint as __Endpoint,
HeaderBag as __HeaderBag,
@@ -64,12 +77,85 @@ const de_CommandError = async (output: __HttpResponse, context: __SerdeContext):
body: await parseErrorBody(output.body, context),
};
const errorCode = loadSmithyRpcV2CborErrorCode(output, parsedOutput.body);
- const parsedBody = parsedOutput.body;
- return throwDefaultError({
- output,
- parsedBody,
- errorCode,
- }) as never;
+ switch (errorCode) {
+ case "CodedThrottlingError":
+ case "org.xyz.v1#CodedThrottlingError":
+ throw await de_CodedThrottlingErrorRes(parsedOutput, context);
+ case "HaltError":
+ case "org.xyz.v1#HaltError":
+ throw await de_HaltErrorRes(parsedOutput, context);
+ case "MysteryThrottlingError":
+ case "org.xyz.v1#MysteryThrottlingError":
+ throw await de_MysteryThrottlingErrorRes(parsedOutput, context);
+ case "RetryableError":
+ case "org.xyz.v1#RetryableError":
+ throw await de_RetryableErrorRes(parsedOutput, context);
+ default:
+ const parsedBody = parsedOutput.body;
+ return throwDefaultError({
+ output,
+ parsedBody,
+ errorCode,
+ }) as never;
+ }
+};
+
+/**
+ * deserializeRpcv2cborCodedThrottlingErrorRes
+ */
+const de_CodedThrottlingErrorRes = async (
+ parsedOutput: any,
+ context: __SerdeContext
+): Promise => {
+ const body = parsedOutput.body;
+ const deserialized: any = _json(body);
+ const exception = new CodedThrottlingError({
+ $metadata: deserializeMetadata(parsedOutput),
+ ...deserialized,
+ });
+ return __decorateServiceException(exception, body);
+};
+
+/**
+ * deserializeRpcv2cborHaltErrorRes
+ */
+const de_HaltErrorRes = async (parsedOutput: any, context: __SerdeContext): Promise => {
+ const body = parsedOutput.body;
+ const deserialized: any = _json(body);
+ const exception = new HaltError({
+ $metadata: deserializeMetadata(parsedOutput),
+ ...deserialized,
+ });
+ return __decorateServiceException(exception, body);
+};
+
+/**
+ * deserializeRpcv2cborMysteryThrottlingErrorRes
+ */
+const de_MysteryThrottlingErrorRes = async (
+ parsedOutput: any,
+ context: __SerdeContext
+): Promise => {
+ const body = parsedOutput.body;
+ const deserialized: any = _json(body);
+ const exception = new MysteryThrottlingError({
+ $metadata: deserializeMetadata(parsedOutput),
+ ...deserialized,
+ });
+ return __decorateServiceException(exception, body);
+};
+
+/**
+ * deserializeRpcv2cborRetryableErrorRes
+ */
+const de_RetryableErrorRes = async (parsedOutput: any, context: __SerdeContext): Promise => {
+ const body = parsedOutput.body;
+ const deserialized: any = _json(body);
+ const exception = new RetryableError({
+ $metadata: deserializeMetadata(parsedOutput),
+ ...deserialized,
+ });
+ return __decorateServiceException(exception, body);
};
/**
@@ -82,6 +168,8 @@ const se_GetNumbersRequest = (input: GetNumbersRequest, context: __SerdeContext)
});
};
+// de_CodedThrottlingError omitted.
+
/**
* deserializeRpcv2cborGetNumbersResponse
*/
@@ -92,6 +180,12 @@ const de_GetNumbersResponse = (output: any, context: __SerdeContext): GetNumbers
}) as any;
};
+// de_HaltError omitted.
+
+// de_MysteryThrottlingError omitted.
+
+// de_RetryableError omitted.
+
const deserializeMetadata = (output: __HttpResponse): __ResponseMetadata => ({
httpStatusCode: output.statusCode,
requestId:
diff --git a/private/util-test/src/test-http-handler.ts b/private/util-test/src/test-http-handler.ts
index f3ddd6cff7e..17787d29613 100644
--- a/private/util-test/src/test-http-handler.ts
+++ b/private/util-test/src/test-http-handler.ts
@@ -1,5 +1,5 @@
import type { HttpHandler, HttpRequest, HttpResponse } from "@smithy/protocol-http";
-import type { Client, RequestHandler, RequestHandlerOutput } from "@smithy/types";
+import type { Client, HttpHandlerOptions, RequestHandler, RequestHandlerOutput } from "@smithy/types";
import { expect } from "vitest";
/**
@@ -28,60 +28,58 @@ export type HttpRequestMatcher = {
log?: boolean;
};
-/**
- * @internal
- */
-const MOCK_CREDENTIALS = {
- accessKeyId: "MOCK_ACCESS_KEY_ID",
- secretAccessKey: "MOCK_SECRET_ACCESS_KEY_ID",
-};
-
-type TestHttpHandlerConfig = object;
-
/**
* Supplied to test clients to assert correct requests.
* @internal
*/
-export class TestHttpHandler implements HttpHandler {
+export class TestHttpHandler implements HttpHandler {
private static WATCHER = Symbol("TestHttpHandler_WATCHER");
- private originalSend?: Client["send"];
+
+ public readonly matchers: HttpRequestMatcher[];
+
+ private originalSend?: Function;
private originalRequestHandler?: RequestHandler;
private client?: Client;
+ private responseQueue: HttpResponse[] = [];
private assertions = 0;
- public constructor(public readonly matcher: HttpRequestMatcher) {}
+ public constructor(...matchers: HttpRequestMatcher[]) {
+ this.matchers = matchers;
+ const RESERVED_ENVIRONMENT_VARIABLES = {
+ AWS_DEFAULT_REGION: 1,
+ AWS_REGION: 1,
+ AWS_PROFILE: 1,
+ AWS_ACCESS_KEY_ID: 1,
+ AWS_SECRET_ACCESS_KEY: 1,
+ AWS_SESSION_TOKEN: 1,
+ AWS_CREDENTIAL_EXPIRATION: 1,
+ AWS_CREDENTIAL_SCOPE: 1,
+ AWS_EC2_METADATA_DISABLED: 1,
+ AWS_WEB_IDENTITY_TOKEN_FILE: 1,
+ AWS_ROLE_ARN: 1,
+ AWS_CONTAINER_CREDENTIALS_FULL_URI: 1,
+ AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: 1,
+ AWS_CONTAINER_AUTHORIZATION_TOKEN: 1,
+ AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: 1,
+ };
+ for (const key in RESERVED_ENVIRONMENT_VARIABLES) {
+ delete process.env[key];
+ }
+ process.env.AWS_ACCESS_KEY_ID = "INTEGRATION_TEST_MOCK";
+ process.env.AWS_SECRET_ACCESS_KEY = "INTEGRATION_TEST_MOCK";
+ }
/**
* @param client - to watch for requests.
- * @param matcher - optional override of this instance's matchers.
+ * @param matchers - optional override of this instance's matchers.
*
* Temporarily hooks the client.send call to check the outgoing request.
*/
- public watch(client: Client, matcher: HttpRequestMatcher = this.matcher) {
+ public watch(client: Client): TestHttpHandler {
this.client = client;
- this.originalRequestHandler = client.config.originalRequestHandler;
- // mock credentials to avoid default chain lookup.
- client.config.credentials = async () => MOCK_CREDENTIALS;
- client.config.credentialDefaultProvider = () => {
- return async () => {
- return MOCK_CREDENTIALS;
- };
- };
- const signerProvider = client.config.signer;
- if (typeof signerProvider === "function") {
- client.config.signer = async () => {
- const _signer = await signerProvider();
- if (typeof _signer.credentialProvider === "function") {
- // signer is instance of SignatureV4
- _signer.credentialProvider = async () => {
- return MOCK_CREDENTIALS;
- };
- }
- return _signer;
- };
- }
+ this.originalRequestHandler = client.config.requestHandler;
- client.config.requestHandler = new TestHttpHandler(matcher);
+ client.config.requestHandler = this;
if (!(client as any)[TestHttpHandler.WATCHER]) {
(client as any)[TestHttpHandler.WATCHER] = true;
const originalSend = (this.originalSend = client.send as any);
@@ -94,14 +92,27 @@ export class TestHttpHandler implements HttpHandler {
});
};
}
+
+ return this;
+ }
+
+ /**
+ * @param httpResponses - to enqueue for mock responses.
+ */
+ public respondWith(...httpResponses: HttpResponse[]): TestHttpHandler {
+ this.responseQueue.push(...httpResponses);
+ return this;
}
/**
* @throws TestHttpHandlerSuccess to indicate success (only way to control it).
* @throws Error any other exception to indicate failure.
*/
- public async handle(request: HttpRequest): Promise> {
- const m = this.matcher;
+ public async handle(
+ request: HttpRequest,
+ handlerOptions?: HttpHandlerOptions
+ ): Promise> {
+ const m = this.matchers.length > 1 ? this.matchers.shift()! : this.matchers[0];
if (m.log) {
console.log(request);
@@ -111,9 +122,9 @@ export class TestHttpHandler implements HttpHandler {
this.check(m.hostname, request.hostname);
this.check(m.port, request.port);
this.check(m.path, request.path);
- this.checkAll(m.query, request.query);
+ this.checkAll(m.query ?? {}, request.query, "query");
- this.checkAll(m.headers, request.headers);
+ this.checkAll(m.headers ?? {}, request.headers, "header");
this.check(m.body, request.body);
this.check(m.method, request.method);
@@ -121,6 +132,18 @@ export class TestHttpHandler implements HttpHandler {
throw new Error("Request handled with no assertions, empty matcher?");
}
+ if (this.responseQueue.length > 1) {
+ return {
+ response: this.responseQueue.shift()!,
+ };
+ } else {
+ if (this.responseQueue.length === 1) {
+ return {
+ response: this.responseQueue[0],
+ };
+ }
+ }
+
throw new TestHttpHandlerSuccess();
}
@@ -129,10 +152,9 @@ export class TestHttpHandler implements HttpHandler {
(this.client as any).send = this.originalSend as any;
}
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- updateHttpClientConfig(key: keyof TestHttpHandlerConfig, value: TestHttpHandlerConfig[typeof key]): void {}
+ updateHttpClientConfig(key: never, value: never): void {}
- httpHandlerConfigs(): TestHttpHandlerConfig {
+ httpHandlerConfigs() {
return {};
}
@@ -167,7 +189,11 @@ export class TestHttpHandler implements HttpHandler {
this.assertions++;
}
- private checkAll(matchers?: Record | Map, observed?: any) {
+ private checkAll(
+ matchers: Record | Map,
+ observed: any,
+ type: "header" | "query"
+ ) {
if (matchers == null) {
return;
}
@@ -179,7 +205,11 @@ export class TestHttpHandler implements HttpHandler {
if (key.startsWith("/") && key.endsWith("/")) {
key = new RegExp(key);
} else {
- this.check(matcher, observed[key]);
+ const matchingValue =
+ type === "header"
+ ? observed[Object.keys(observed).find((k) => k.toLowerCase() === String(key).toLowerCase()) ?? ""]
+ : observed[key];
+ this.check(matcher, matchingValue);
}
}
if (key instanceof RegExp) {
@@ -209,8 +239,8 @@ export class TestHttpHandlerSuccess extends Error {
*/
export const requireRequestsFrom = (client: Client) => {
return {
- toMatch(matcher: HttpRequestMatcher) {
- return new TestHttpHandler(matcher).watch(client);
+ toMatch(...matchers: HttpRequestMatcher[]) {
+ return new TestHttpHandler(...matchers).watch(client);
},
};
};
diff --git a/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy b/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy
index 4fdba604c94..a7e7fd3cf68 100644
--- a/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy
+++ b/smithy-typescript-protocol-test-codegen/model/my-local-model/main.smithy
@@ -18,6 +18,10 @@ operation GetNumbers {
input: GetNumbersRequest
output: GetNumbersResponse
errors: [
+ CodedThrottlingError,
+ MysteryThrottlingError,
+ RetryableError,
+ HaltError
]
}
@@ -31,4 +35,20 @@ structure GetNumbersRequest {
structure GetNumbersResponse {
bigDecimal: BigDecimal
bigInteger: BigInteger
-}
\ No newline at end of file
+}
+
+@error("client")
+@retryable(throttling: true)
+@httpError(429)
+structure CodedThrottlingError {}
+
+@error("client")
+@retryable(throttling: true)
+structure MysteryThrottlingError {}
+
+@error("client")
+@retryable()
+structure RetryableError {}
+
+@error("client")
+structure HaltError {}
\ No newline at end of file