Skip to content
Merged
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
20 changes: 6 additions & 14 deletions clients/client-cloudwatch/test/e2e/CloudWatch.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,10 @@ describe(CloudWatch.name, () => {
}),
};

it("can make requests with AWS Query protocol", async () => {
const dashes = await cloudwatch.query.listDashboards();
expect(dashes.DashboardEntries ?? []).toBeInstanceOf(Array);
});

it("can make requests with Smithy RPCv2 CBOR protocol", async () => {
const dashes = await cloudwatch.cbor.listDashboards();
expect(dashes.DashboardEntries ?? []).toBeInstanceOf(Array);
});

it("can make requests with AWS JSON RPC protocol", async () => {
const dashes = await cloudwatch.json.listDashboards();
expect(dashes.DashboardEntries ?? []).toBeInstanceOf(Array);
});
for (const client of Object.values(cloudwatch)) {
it(`can make requests with ${client.config.protocol.constructor.name}`, async () => {
const dashes = await cloudwatch.query.listDashboards();
expect(dashes.DashboardEntries ?? []).toBeInstanceOf(Array);
});
}
});
141 changes: 124 additions & 17 deletions clients/client-cloudwatch/test/e2e/query-compatibility.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,132 @@
import { CloudWatchClient, GetDashboardCommand } from "@aws-sdk/client-cloudwatch";
import { beforeAll, describe, expect, test as it } from "vitest";
import {
CloudWatchClient,
CloudWatchServiceException,
GetDashboardCommand,
GetInsightRuleReportCommand,
MissingRequiredParameterException,
PutMetricAlarmCommand,
ResourceNotFound,
} from "@aws-sdk/client-cloudwatch";
import { AwsJson1_0Protocol, AwsQueryProtocol, AwsSmithyRpcV2CborProtocol } from "@aws-sdk/core";
import { describe, expect, test as it } from "vitest";

describe("CloudWatch Query Compatibility E2E", () => {
let client: CloudWatchClient;

beforeAll(async () => {
client = new CloudWatchClient({
const cloudwatch = {
cbor: new CloudWatchClient({
region: "us-west-2",
protocol: new AwsSmithyRpcV2CborProtocol({
defaultNamespace: "com.amazonaws.cloudwatch",
awsQueryCompatible: true,
}),
}),
query: new CloudWatchClient({
region: "us-west-2",
protocol: new AwsQueryProtocol({
defaultNamespace: "com.amazonaws.cloudwatch",
xmlNamespace: "http://monitoring.amazonaws.com/doc/2010-08-01/",
version: "2010-08-01",
}),
}),
json: new CloudWatchClient({
region: "us-west-2",
protocol: new AwsJson1_0Protocol({
defaultNamespace: "com.amazonaws.cloudwatch",
serviceTarget: "GraniteServiceVersion20100801",
awsQueryCompatible: true,
}),
}),
};

for (const client of Object.values(cloudwatch)) {
const ctorName = client.config.protocol.constructor.name;

it(`resolve errors with the query compat header and not the __type field (${ctorName})`, async () => {
const error = await client
.send(
new GetDashboardCommand({
DashboardName: "does-not-exist",
})
)
.catch((_) => _);

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(CloudWatchServiceException);
expect(error).toBeInstanceOf(ResourceNotFound);

expect(error.constructor.prototype.name).toBe("Error");
expect(error.constructor.name).toBe("ResourceNotFound");
expect(error.name).toBe("ResourceNotFound");

expect(error.message).toBe("Dashboard does-not-exist does not exist");
expect(error.Type).toEqual("Sender");
expect(error.Code).toEqual("ResourceNotFound");
expect(error.Error).toEqual({
Type: "Sender",
Code: "ResourceNotFound",
Message: "Dashboard does-not-exist does not exist",
});
expect(error.$metadata.httpStatusCode).toBe(404);
});
});

it("AmbiguousErrorResolution", async () => {
const command = new GetDashboardCommand({
DashboardName: "foo",
it(`have consistent error structure (modeled title-case 'Message') ${ctorName}`, async () => {
const error = await client
.send(
new PutMetricAlarmCommand({
AlarmName: "",
ComparisonOperator: "GreaterThanThreshold",
EvaluationPeriods: 5,
MetricName: "CPUUtilization",
Namespace: "AWS/EC2",
Period: 60,
Statistic: "Average",
Threshold: 50.0,
})
)
.catch((_) => _);

const msg =
`1 validation error detected: Value '' at 'alarmName' failed to satisfy ` +
`constraint: Member must have length greater than or equal to 1`;

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(CloudWatchServiceException);

expect(error.constructor.prototype.name).toBe("Error");
expect(error.constructor.name).toBe("CloudWatchServiceException");
expect(error.name).toBe("ValidationError");

expect(error.message).toBe(msg);
expect(error.Type).toEqual("Sender");
expect(error.Code).toEqual("ValidationError");
expect(error.Error).toEqual({
Type: "Sender",
Code: "ValidationError",
Message: msg,
});
expect(error.$metadata.httpStatusCode).toBe(400);
});

try {
await client.send(command);
fail("Expected ResourceNotFound error");
} catch (error: any) {
expect(error.name).toBe("ResourceNotFound");
}
});
it(`have consistent error structure (modeled lowercase 'message') ${ctorName}`, async () => {
const error = await client.send(new GetInsightRuleReportCommand({} as any)).catch((_) => _);
const msg = `MISSING_RULE_NAME: The RuleName parameter must be present.`;

expect(error).toBeInstanceOf(Error);
expect(error).toBeInstanceOf(CloudWatchServiceException);
expect(error).toBeInstanceOf(MissingRequiredParameterException);

expect(error.constructor.prototype.name).toBe("Error");
expect(error.constructor.name).toBe("MissingRequiredParameterException");
expect(error.name).toBe("MissingRequiredParameterException");

expect(error.message).toBe(msg);
expect(error.Type).toEqual("Sender");
expect(error.Code).toEqual("MissingParameter");
expect(error.Error).toEqual({
Type: "Sender",
Code: "MissingParameter",
Message: msg,
});
expect(error.$metadata.httpStatusCode).toBe(400);
});
}
});
35 changes: 31 additions & 4 deletions packages/core/src/submodules/protocols/ProtocolLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,23 @@ export class ProtocolLib {
): E {
if (this.queryCompat) {
const msg = (exception as any).Message ?? additions.Message;
const error = decorateServiceException(exception, additions);
const error: any = decorateServiceException(exception, additions);
if (msg) {
(error as any).Message = msg;
(error as any).message = msg;
error.message = msg;
}

error.Error = {
...error.Error,
Type: error.Error.Type,
Code: error.Error.Code,
Message: error.Error.message ?? error.Error.Message ?? msg,
};

const reqId = error.$metadata.requestId;
if (reqId) {
error.RequestId = reqId;
}

return error;
}
return decorateServiceException(exception, additions);
Expand All @@ -138,7 +150,7 @@ export class ProtocolLib {
} as any;
Object.assign(output, Error);
for (const [k, v] of entries) {
Error[k] = v;
Error[k === "message" ? "Message" : k] = v;
}
delete Error.__type;
output.Error = Error;
Expand All @@ -161,4 +173,19 @@ export class ProtocolLib {
errorData.Code = queryCompatErrorData.Code;
}
}

/**
* Finds the canonical modeled error using the awsQueryError alias.
* @param registry - service error registry.
* @param errorName - awsQueryError name or regular qualified shapeId.
*/
public findQueryCompatibleError(registry: TypeRegistry, errorName: string) {
try {
return registry.getSchema(errorName) as StaticErrorSchema;
} catch (e) {
return registry.find(
(schema) => (NormalizedSchema.of(schema).getMergedTraits().awsQueryError as any)?.[0] === errorName
) as StaticErrorSchema;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,18 @@ describe(AwsSmithyRpcV2CborProtocol.name, () => {
// set by deserializer middleware, not Protocol.
expect(error.$response).toEqual(undefined);

expect(error.Code).toEqual(MyQueryError.name);
expect(error.Error.Code).toEqual(MyQueryError.name);

expect(error.Message).toEqual("oh no");
expect(error.Prop2).toEqual(9999);

expect(error.Error.Message).toEqual("oh no");
expect(error.Error.Prop2).toEqual(9999);

expect(error).toMatchObject({
$fault: "client",
Message: "oh no",
message: "oh no",
Prop2: 9999,
Error: {
Code: "MyQueryError",
Code: MyQueryError.name,
Message: "oh no",
Type: "Client",
Prop2: 9999,
},
Type: "Client",
Code: "MyQueryError",
Code: MyQueryError.name,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,21 @@ export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
if (this.awsQueryCompatible) {
this.mixin.setQueryCompatError(dataObject, response);
}
const errorName = loadSmithyRpcV2CborErrorCode(response, dataObject) ?? "Unknown";
const errorName = (() => {
const compatHeader = response.headers["x-amzn-query-error"];
if (compatHeader && this.awsQueryCompatible) {
return compatHeader.split(";")[0];
}
return loadSmithyRpcV2CborErrorCode(response, dataObject) ?? "Unknown";
})();

const { errorSchema, errorMetadata } = await this.mixin.getErrorSchemaOrThrowBaseException(
errorName,
this.options.defaultNamespace,
response,
dataObject,
metadata
metadata,
this.awsQueryCompatible ? this.mixin.findQueryCompatibleError : undefined
);

const ns = NormalizedSchema.of(errorSchema);
Expand All @@ -78,7 +85,9 @@ export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {

const output = {} as any;
for (const [name, member] of ns.structIterator()) {
output[name] = this.deserializer.readValue(member, dataObject[name]);
if (dataObject[name] != null) {
output[name] = this.deserializer.readValue(member, dataObject[name]);
}
}

if (this.awsQueryCompatible) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,28 +83,18 @@ describe(AwsJsonRpcProtocol.name, () => {
// set by deserializer middleware, not protocol
expect(error.$response).toEqual(undefined);

expect(error.Code).toEqual(MyQueryError.name);
expect(error.Error.Code).toEqual(MyQueryError.name);

expect(error.Message).toEqual("oh no");
expect(error.Prop2).toEqual(9999);

expect(error.Error.Message).toEqual("oh no");
expect(error.Error.Prop2).toEqual(9999);

expect(error).toMatchObject({
$fault: "client",
Message: "oh no",
message: "oh no",
Prop2: 9999,
Error: {
Code: "MyQueryError",
Code: MyQueryError.name,
Message: "oh no",
Type: "Client",
Prop2: 9999,
},
Type: "Client",
Code: "MyQueryError",
Code: MyQueryError.name,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
this.options.defaultNamespace,
response,
dataObject,
metadata
metadata,
this.awsQueryCompatible ? this.mixin.findQueryCompatibleError : undefined
);

const ns = NormalizedSchema.of(errorSchema);
Expand All @@ -120,8 +121,9 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {

const output = {} as any;
for (const [name, member] of ns.structIterator()) {
const target = member.getMergedTraits().jsonName ?? name;
output[name] = this.codec.createDeserializer().readObject(member, dataObject[target]);
if (dataObject[name] != null) {
output[name] = this.codec.createDeserializer().readObject(member, dataObject[name]);
}
}

if (this.awsQueryCompatible) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,22 +161,16 @@ export class AwsQueryProtocol extends RpcProtocol {
response,
errorData,
metadata,
(registry: TypeRegistry, errorName: string) => {
try {
return registry.getSchema(errorName) as StaticErrorSchema;
} catch (e) {
return registry.find(
(schema) => (NormalizedSchema.of(schema).getMergedTraits().awsQueryError as any)?.[0] === errorName
) as StaticErrorSchema;
}
}
this.mixin.findQueryCompatibleError
);

const ns = NormalizedSchema.of(errorSchema);
const ErrorCtor = TypeRegistry.for(errorSchema[1]).getErrorCtor(errorSchema) ?? Error;
const exception = new ErrorCtor(message);

const output = {
Type: errorData.Error.Type,
Code: errorData.Error.Code,
Error: errorData.Error,
} as any;

Expand Down
Loading