Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions scripts/generate-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,16 @@ const generateServiceIndex = (
});
code += " },\n";
}
if (
metadata.retryableErrors &&
Object.keys(metadata.retryableErrors).length > 0
) {
code += " retryableErrors: {\n";
Object.entries(metadata.retryableErrors).forEach(([errorName, error]) => {
code += ` "${errorName}": ${JSON.stringify(error)},\n`;
});
code += " },\n";
}
code += "} as const satisfies ServiceMetadata;\n\n";

// // Re-export all types from types.ts for backward compatibility
Expand Down Expand Up @@ -1707,6 +1717,42 @@ const generateServiceTypes = (serviceName: string, manifest: Manifest) =>
// Extract operation HTTP mappings and trait mappings
let operationMappings: Record<string, any> = {};

const extractRetryableError = (
shapeId: string,
): [string, Record<string, string>] => {
const shape = manifest.shapes[shapeId];
if (!shape || shape.type !== "structure" || !shape.members)
return ["", {}];

if (shape.traits["smithy.api#retryable"] == null) return ["", {}];
// const isEmptyTrait = Object.keys(shape.members).length === 0;

// return shape;
const name = extractShapeName(shapeId);

return [
name,
{
retryAfterSeconds:
shape?.members?.retryAfterSeconds?.traits?.[
"smithy.api#httpHeader"
],
// members: Object.entries(shape.members).reduce(
// (acc, [key, value]) => {
// if (value?.traits?.["smithy.api#httpHeader"]) {
// acc[key] = value?.traits?.["smithy.api#httpHeader"];
// }
// return acc;
// },
// {} as Record<string, string>,
// ),
// ...(Object.keys(shape.traits["smithy.api#retryable"]).length > 0
// ? shape.traits["smithy.api#retryable"]
// : {}),
},
];
};

const extractHttpTraits = (shapeId: string): Record<string, string> => {
const shape = manifest.shapes[shapeId];
if (!shape || shape.type !== "structure" || !shape.members) return {};
Expand Down Expand Up @@ -1742,6 +1788,8 @@ const generateServiceTypes = (serviceName: string, manifest: Manifest) =>
) as Record<string, string>;
};

let retryableErrors = {};

if (protocol === "restJson1") {
for (const operation of operations) {
const httpTrait = operation.shape.traits?.["smithy.api#http"];
Expand All @@ -1753,6 +1801,13 @@ const generateServiceTypes = (serviceName: string, manifest: Manifest) =>
const outputTraits = operation.shape.output
? extractHttpTraits(operation.shape.output.target)
: {};
const errorList =
operation?.shape?.errors
?.map((e) => extractRetryableError(e.target))
.filter((error) => Object.keys(error[1]).length > 0) ?? [];
for (const error of errorList) {
retryableErrors[error[0]] = error[1];
}

if (Object.keys(outputTraits).length > 0) {
// Store both HTTP mapping and trait mappings
Expand Down Expand Up @@ -1839,6 +1894,7 @@ const generateServiceTypes = (serviceName: string, manifest: Manifest) =>
...(Object.keys(operationMappings).length > 0 && {
operations: operationMappings,
}),
retryableErrors,
};

return { code, metadata };
Expand Down
100 changes: 95 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import { Credentials, fromStaticCredentials } from "./credentials.ts";
import type { AwsErrorMeta } from "./error.ts";
import { DefaultFetch, Fetch } from "./fetch.service.ts";
import type { ProtocolHandler } from "./protocols/interface.ts";
import * as Schedule from "effect/Schedule";
import * as Duration from "effect/Duration";
import { pipe } from "effect";
import * as Context from "effect/Context";

export const retryableErrorTags = [
"InternalFailure",
"RequestExpired",
"ServiceException",
"ServiceUnavailable",
"ThrottlingException",
"TooManyRequestsException",
];

const errorTags: {
[serviceName: string]: {
Expand All @@ -17,13 +30,27 @@ const errorTags: {
function createServiceError(
serviceName: string,
errorName: string,
errorMeta: AwsErrorMeta & { message?: string },
errorMeta: AwsErrorMeta & {
message?: string;
retryable:
| {
retryAfterSeconds?: number;
}
| false;
},
) {
// Create a tagged error dynamically with the correct error name
return new ((errorTags[serviceName] ??= {})[errorName] ??= (() =>
Data.TaggedError(errorName)<AwsErrorMeta & { message?: string }>)())(
errorMeta,
);
Data.TaggedError(errorName)<
AwsErrorMeta & {
message?: string;
retryable:
| {
retryAfterSeconds?: number;
}
| false;
}
>)())(errorMeta);
}

// Types
Expand All @@ -46,6 +73,7 @@ export interface ServiceMetadata {
readonly outputTraits?: Record<string, string>;
}
>; // Operation mappings for restJson1 and trait mappings
retryableErrors?: Record<string, { retryAfterSeconds?: string }>;
}

export interface AwsCredentials {
Expand Down Expand Up @@ -213,6 +241,7 @@ export function createServiceProxy<T>(
response,
statusCode,
response.headers,
metadata,
),
);

Expand All @@ -229,14 +258,75 @@ export function createServiceProxy<T>(
{
...errorMeta,
message: parsedError.message,
retryable: parsedError.retryable ?? false,
},
),
);
}
});
return program;
return Effect.gen(function* () {
const retryPolicy = yield* Effect.serviceOption(RetryPolicy);
const randomNumber = Option.getOrUndefined(retryPolicy) ?? {
maxRetries: 5,
delay: Duration.millis(100),
};
return yield* withRetry(program, randomNumber);
});
};
},
},
) as T;
}

class RetryPolicy extends Context.Tag("RetryPolicy")<
RetryPolicy,
{
maxRetries: number;
delay: Duration.Duration;
}
>() {}

const withRetry = <A>(
operation: Effect.Effect<
A,
{
readonly _tag: string;
retryable:
| {
retryAfterSeconds?: number;
}
| false;
}
>,
randomNumber: {
maxRetries: number;
delay: Duration.Duration;
},
) =>
pipe(
operation,
Effect.retry({
while: (error) =>
error.retryable !== false || retryableErrorTags.includes(error._tag),
schedule: pipe(
Schedule.exponential(randomNumber.delay),
Schedule.passthrough,
Schedule.addDelay((err) => {
const error = err as {
readonly _tag: string;
retryable:
| {
retryAfterSeconds?: number;
}
| false;
};

return typeof error?.retryable === "object" &&
error?.retryable?.retryAfterSeconds != null
? Duration.seconds(error?.retryable?.retryAfterSeconds)
: Duration.zero;
}),
Schedule.compose(Schedule.recurs(randomNumber.maxRetries)),
),
}),
);
1 change: 1 addition & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Data from "effect/Data";
export interface AwsErrorMeta {
readonly statusCode: number;
readonly requestId?: string;
readonly retryAfterSeconds?: number;
}

// Common AWS errors that can occur across all services
Expand Down
9 changes: 8 additions & 1 deletion src/protocols/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export interface ParsedError {
readonly errorType: string;
readonly message: string;
readonly requestId?: string;
readonly retryable?:
| {
retryAfterSeconds?: number;
retryAttempts?: number;
}
| false;
}

export interface ProtocolRequest {
Expand Down Expand Up @@ -37,6 +43,7 @@ export interface ProtocolHandler {
parseError(
responseText: Response,
statusCode: number,
headers?: Headers,
headers: Headers,
serviceMetadata: ServiceMetadata,
): Promise<ParsedError>;
}
13 changes: 12 additions & 1 deletion src/protocols/rest-json-1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export class RestJson1Handler implements ProtocolHandler {
async parseError(
response: Response,
_statusCode: number,
headers?: Headers,
headers: Headers,
metadata: ServiceMetadata,
): Promise<ParsedError> {
let errorData: any;
const responseText = await response.text();
Expand All @@ -154,11 +155,21 @@ export class RestJson1Handler implements ProtocolHandler {
headers?.get("x-amzn-requestid") ||
headers?.get("x-amz-request-id") ||
undefined;
const retryable =
metadata?.retryableErrors?.[errorType] != null
? {
retryAfterSeconds:
headers?.get(
metadata?.retryableErrors?.[errorTypes]?.retryAfterSeconds,
) ?? undefined,
}
: false;

return {
errorType,
message,
requestId,
retryable,
};
}

Expand Down
3 changes: 1 addition & 2 deletions src/protocols/rest-xml.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as FastCheck from "effect/FastCheck";
import type { ServiceMetadata } from "../client.ts";
import type {
ParsedError,
Expand Down Expand Up @@ -115,7 +114,7 @@ export class RestXmlHandler implements ProtocolHandler {

async parseError(
response: Response,
statusCode: number,
_statusCode: number,
headers?: Headers,
): Promise<ParsedError> {
const responseText = await response.text();
Expand Down
5 changes: 5 additions & 0 deletions src/services/accessanalyzer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ const metadata = {
UpdateAnalyzer: "PUT /analyzer/{analyzerName}",
UpdateArchiveRule: "PUT /analyzer/{analyzerName}/archive-rule/{ruleName}",
},
retryableErrors: {
InternalServerException: { retryAfterSeconds: "Retry-After" },
ThrottlingException: { retryAfterSeconds: "Retry-After" },
UnprocessableEntityException: {},
},
} as const satisfies ServiceMetadata;

export type _AccessAnalyzer = _AccessAnalyzerClient;
Expand Down
4 changes: 4 additions & 0 deletions src/services/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const metadata = {
PutContactInformation: "POST /putContactInformation",
StartPrimaryEmailUpdate: "POST /startPrimaryEmailUpdate",
},
retryableErrors: {
InternalServerException: {},
TooManyRequestsException: {},
},
} as const satisfies ServiceMetadata;

export type _Account = _AccountClient;
Expand Down
4 changes: 4 additions & 0 deletions src/services/amp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ const metadata = {
UpdateWorkspaceConfiguration:
"PATCH /workspaces/{workspaceId}/configuration",
},
retryableErrors: {
InternalServerException: { retryAfterSeconds: "Retry-After" },
ThrottlingException: { retryAfterSeconds: "Retry-After" },
},
} as const satisfies ServiceMetadata;

export type _amp = _ampClient;
Expand Down
5 changes: 5 additions & 0 deletions src/services/app-mesh/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ const metadata = {
},
},
},
retryableErrors: {
InternalServerErrorException: {},
ServiceUnavailableException: {},
TooManyRequestsException: {},
},
} as const satisfies ServiceMetadata;

export type _AppMesh = _AppMeshClient;
Expand Down
4 changes: 4 additions & 0 deletions src/services/appfabric/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ const metadata = {
UpdateIngestionDestination:
"PATCH /appbundles/{appBundleIdentifier}/ingestions/{ingestionIdentifier}/ingestiondestinations/{ingestionDestinationIdentifier}",
},
retryableErrors: {
InternalServerException: { retryAfterSeconds: "Retry-After" },
ThrottlingException: { retryAfterSeconds: "Retry-After" },
},
} as const satisfies ServiceMetadata;

export type _AppFabric = _AppFabricClient;
Expand Down
4 changes: 4 additions & 0 deletions src/services/artifact/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ const metadata = {
ListReports: "GET /v1/report/list",
PutAccountSettings: "PUT /v1/account-settings/put",
},
retryableErrors: {
InternalServerException: { retryAfterSeconds: "Retry-After" },
ThrottlingException: { retryAfterSeconds: "Retry-After" },
},
} as const satisfies ServiceMetadata;

export type _Artifact = _ArtifactClient;
Expand Down
4 changes: 4 additions & 0 deletions src/services/bedrock-agentcore-control/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ const metadata = {
"POST /identities/UpdateOauth2CredentialProvider",
UpdateWorkloadIdentity: "POST /identities/UpdateWorkloadIdentity",
},
retryableErrors: {
ServiceException: {},
ThrottledException: {},
},
} as const satisfies ServiceMetadata;

export type _BedrockAgentCoreControl = _BedrockAgentCoreControlClient;
Expand Down
Loading
Loading