Skip to content

Commit 6de803d

Browse files
authored
fix(core/protocols): decorate service exceptions with unmodeled fields (#7504)
1 parent 99f5f06 commit 6de803d

15 files changed

+307
-120
lines changed

clients/client-s3/test/e2e/S3.e2e.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import "@aws-sdk/signature-v4-crt";
22

3+
import { getE2eTestResources } from "@aws-sdk/aws-util-test/src";
34
import { ChecksumAlgorithm, S3 } from "@aws-sdk/client-s3";
45
import { afterAll, afterEach, beforeAll, describe, expect, test as it } from "vitest";
56

67
import { createBuffer } from "./helpers";
7-
import { getE2eTestResources } from "@aws-sdk/aws-util-test/src";
88

99
let Key = `${Date.now()}`;
1010

@@ -246,5 +246,19 @@ describe("@aws-sdk/client-s3", () => {
246246
expect(result.$metadata.httpStatusCode).toEqual(200);
247247
expect(result.Contents).toBeInstanceOf(Array);
248248
});
249+
250+
describe("error handling", () => {
251+
it("should decorate exceptions with unmodeled error fields", async () => {
252+
const error = await client
253+
.abortMultipartUpload({
254+
Bucket,
255+
Key: "nonexistent-key",
256+
UploadId: "uploadId",
257+
})
258+
.catch((e) => e);
259+
260+
expect((error as any).UploadId).toEqual("uploadId");
261+
});
262+
});
249263
});
250264
}, 60_000);

packages/core/src/submodules/protocols/ProtocolLib.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { NormalizedSchema, TypeRegistry } from "@smithy/core/schema";
2+
import { decorateServiceException, ServiceException as SDKBaseServiceException } from "@smithy/smithy-client";
23
import type { HttpResponse as IHttpResponse, MetadataBearer, ResponseMetadata, StaticErrorSchema } from "@smithy/types";
34

45
/**
56
* @internal
67
*/
78
type ErrorMetadataBearer = MetadataBearer & {
8-
$response: IHttpResponse;
9+
// $response is set by the deserializer middleware, not Protocol.
910
$fault: "client" | "server";
1011
};
1112

@@ -15,6 +16,8 @@ type ErrorMetadataBearer = MetadataBearer & {
1516
* @internal
1617
*/
1718
export class ProtocolLib {
19+
public constructor(private queryCompat = false) {}
20+
1821
/**
1922
* This is only for REST protocols.
2023
*
@@ -74,7 +77,6 @@ export class ProtocolLib {
7477

7578
const errorMetadata: ErrorMetadataBearer = {
7679
$metadata: metadata,
77-
$response: response,
7880
$fault: response.statusCode < 500 ? ("client" as const) : ("server" as const),
7981
};
8082

@@ -90,10 +92,32 @@ export class ProtocolLib {
9092
const baseExceptionSchema = synthetic.getBaseException();
9193
if (baseExceptionSchema) {
9294
const ErrorCtor = synthetic.getErrorCtor(baseExceptionSchema) ?? Error;
93-
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
95+
throw this.decorateServiceException(
96+
Object.assign(new ErrorCtor({ name: errorName }), errorMetadata),
97+
dataObject
98+
);
99+
}
100+
throw this.decorateServiceException(Object.assign(new Error(errorName), errorMetadata), dataObject);
101+
}
102+
}
103+
104+
/**
105+
* Assigns additions onto exception if not already present.
106+
*/
107+
public decorateServiceException<E extends SDKBaseServiceException>(
108+
exception: E,
109+
additions: Record<string, any> = {}
110+
): E {
111+
if (this.queryCompat) {
112+
const msg = (exception as any).Message ?? additions.Message;
113+
const error = decorateServiceException(exception, additions);
114+
if (msg) {
115+
(error as any).Message = msg;
116+
(error as any).message = msg;
94117
}
95-
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
118+
return error;
96119
}
120+
return decorateServiceException(exception, additions);
97121
}
98122

99123
/**

packages/core/src/submodules/protocols/cbor/AwsSmithyRpcV2CborProtocol.spec.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { HttpResponse } from "@smithy/protocol-http";
44
import type { NumericSchema, StringSchema } from "@smithy/types";
55
import { describe, expect, test as it } from "vitest";
66

7+
import { context } from "../test-schema.spec";
78
import { AwsSmithyRpcV2CborProtocol } from "./AwsSmithyRpcV2CborProtocol";
89

910
describe(AwsSmithyRpcV2CborProtocol.name, () => {
@@ -56,16 +57,8 @@ describe(AwsSmithyRpcV2CborProtocol.name, () => {
5657
requestId: undefined,
5758
});
5859

59-
expect(error.$response).toEqual(
60-
new HttpResponse({
61-
body,
62-
headers: {
63-
"x-amzn-query-error": "MyQueryError;Client",
64-
},
65-
reason: undefined,
66-
statusCode: 400,
67-
})
68-
);
60+
// set by deserializer middleware, not Protocol.
61+
expect(error.$response).toEqual(undefined);
6962

7063
expect(error.Code).toEqual(MyQueryError.name);
7164
expect(error.Error.Code).toEqual(MyQueryError.name);
@@ -91,4 +84,41 @@ describe(AwsSmithyRpcV2CborProtocol.name, () => {
9184
Code: "MyQueryError",
9285
});
9386
});
87+
88+
it("decorates service exceptions with unmodeled fields", async () => {
89+
const httpResponse = new HttpResponse({
90+
statusCode: 400,
91+
headers: {},
92+
body: cbor.serialize({
93+
UnmodeledField: "Oh no",
94+
}),
95+
});
96+
97+
const protocol = new AwsSmithyRpcV2CborProtocol({
98+
defaultNamespace: "",
99+
});
100+
101+
const output = await protocol
102+
.deserializeResponse(
103+
{
104+
namespace: "ns",
105+
name: "Empty",
106+
traits: 0,
107+
input: "unit" as const,
108+
output: [3, "ns", "EmptyOutput", 0, [], []],
109+
},
110+
context,
111+
httpResponse
112+
)
113+
.catch((e) => {
114+
return e;
115+
});
116+
117+
expect(output).toMatchObject({
118+
UnmodeledField: "Oh no",
119+
$metadata: {
120+
httpStatusCode: 400,
121+
},
122+
});
123+
});
94124
});

packages/core/src/submodules/protocols/cbor/AwsSmithyRpcV2CborProtocol.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { ProtocolLib } from "../ProtocolLib";
1919
*/
2020
export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
2121
private readonly awsQueryCompatible: boolean;
22-
private readonly mixin = new ProtocolLib();
22+
private readonly mixin: ProtocolLib;
2323

2424
public constructor({
2525
defaultNamespace,
@@ -30,6 +30,7 @@ export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
3030
}) {
3131
super({ defaultNamespace });
3232
this.awsQueryCompatible = !!awsQueryCompatible;
33+
this.mixin = new ProtocolLib(this.awsQueryCompatible);
3334
}
3435

3536
/**
@@ -84,14 +85,17 @@ export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
8485
this.mixin.queryCompatOutput(dataObject, output);
8586
}
8687

87-
throw Object.assign(
88-
exception,
89-
errorMetadata,
90-
{
91-
$fault: ns.getMergedTraits().error,
92-
message,
93-
},
94-
output
88+
throw this.mixin.decorateServiceException(
89+
Object.assign(
90+
exception,
91+
errorMetadata,
92+
{
93+
$fault: ns.getMergedTraits().error,
94+
message,
95+
},
96+
output
97+
),
98+
dataObject
9599
);
96100
}
97101
}

packages/core/src/submodules/protocols/json/AwsJson1_1Protocol.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,40 @@ describe(AwsJson1_1Protocol, () => {
8686
},
8787
});
8888
});
89+
90+
it("decorates service exceptions with unmodeled fields", async () => {
91+
const httpResponse = new HttpResponse({
92+
statusCode: 400,
93+
headers: {},
94+
body: Buffer.from(`{"UnmodeledField":"Oh no"}`),
95+
});
96+
97+
const protocol = new AwsJson1_1Protocol({
98+
defaultNamespace: "",
99+
serviceTarget: "JsonRpc11",
100+
});
101+
102+
const output = await protocol
103+
.deserializeResponse(
104+
{
105+
namespace: "ns",
106+
name: "Empty",
107+
traits: 0,
108+
input: "unit" as const,
109+
output: [3, "ns", "EmptyOutput", 0, [], []],
110+
},
111+
context,
112+
httpResponse
113+
)
114+
.catch((e) => {
115+
return e;
116+
});
117+
118+
expect(output).toMatchObject({
119+
UnmodeledField: "Oh no",
120+
$metadata: {
121+
httpStatusCode: 400,
122+
},
123+
});
124+
});
89125
});

packages/core/src/submodules/protocols/json/AwsJsonRpcProtocol.spec.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,8 @@ describe(AwsJsonRpcProtocol.name, () => {
8080
requestId: undefined,
8181
});
8282

83-
expect(error.$response).toEqual(
84-
new HttpResponse({
85-
body,
86-
headers: {
87-
"x-amzn-query-error": "MyQueryError;Client",
88-
},
89-
reason: undefined,
90-
statusCode: 400,
91-
})
92-
);
83+
// set by deserializer middleware, not protocol
84+
expect(error.$response).toEqual(undefined);
9385

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

packages/core/src/submodules/protocols/json/AwsJsonRpcProtocol.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
2525
protected deserializer: ShapeDeserializer<string | Uint8Array>;
2626
protected serviceTarget: string;
2727
private readonly codec: JsonCodec;
28-
private readonly mixin = new ProtocolLib();
28+
private readonly mixin: ProtocolLib;
2929
private readonly awsQueryCompatible: boolean;
3030

3131
protected constructor({
@@ -51,6 +51,7 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
5151
this.serializer = this.codec.createSerializer();
5252
this.deserializer = this.codec.createDeserializer();
5353
this.awsQueryCompatible = !!awsQueryCompatible;
54+
this.mixin = new ProtocolLib(this.awsQueryCompatible);
5455
}
5556

5657
public async serializeRequest<Input extends object>(
@@ -84,6 +85,9 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
8485

8586
protected abstract getJsonRpcVersion(): "1.1" | "1.0";
8687

88+
/**
89+
* @override
90+
*/
8791
protected async handleError(
8892
operationSchema: OperationSchema,
8993
context: HandlerExecutionContext & SerdeFunctions,
@@ -120,14 +124,17 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
120124
this.mixin.queryCompatOutput(dataObject, output);
121125
}
122126

123-
throw Object.assign(
124-
exception,
125-
errorMetadata,
126-
{
127-
$fault: ns.getMergedTraits().error,
128-
message,
129-
},
130-
output
127+
throw this.mixin.decorateServiceException(
128+
Object.assign(
129+
exception,
130+
errorMetadata,
131+
{
132+
$fault: ns.getMergedTraits().error,
133+
message,
134+
},
135+
output
136+
),
137+
dataObject
131138
);
132139
}
133140
}

packages/core/src/submodules/protocols/json/AwsRestJsonProtocol.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,5 +410,40 @@ describe(AwsRestJsonProtocol.name, () => {
410410
payload: null,
411411
});
412412
});
413+
414+
it("decorates service exceptions with unmodeled fields", async () => {
415+
const httpResponse = new HttpResponse({
416+
statusCode: 400,
417+
headers: {},
418+
body: Buffer.from(`{"UnmodeledField":"Oh no"}`),
419+
});
420+
421+
const protocol = new AwsRestJsonProtocol({
422+
defaultNamespace: "",
423+
});
424+
425+
const output = await protocol
426+
.deserializeResponse(
427+
{
428+
namespace: "ns",
429+
name: "Empty",
430+
traits: 0,
431+
input: "unit" as const,
432+
output: [3, "ns", "EmptyOutput", 0, [], []],
433+
},
434+
context,
435+
httpResponse
436+
)
437+
.catch((e) => {
438+
return e;
439+
});
440+
441+
expect(output).toMatchObject({
442+
UnmodeledField: "Oh no",
443+
$metadata: {
444+
httpStatusCode: 400,
445+
},
446+
});
447+
});
413448
});
414449
});

packages/core/src/submodules/protocols/json/AwsRestJsonProtocol.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,17 @@ export class AwsRestJsonProtocol extends HttpBindingProtocol {
139139
output[name] = this.codec.createDeserializer().readObject(member, dataObject[target]);
140140
}
141141

142-
throw Object.assign(
143-
exception,
144-
errorMetadata,
145-
{
146-
$fault: ns.getMergedTraits().error,
147-
message,
148-
},
149-
output
142+
throw this.mixin.decorateServiceException(
143+
Object.assign(
144+
exception,
145+
errorMetadata,
146+
{
147+
$fault: ns.getMergedTraits().error,
148+
message,
149+
},
150+
output
151+
),
152+
dataObject
150153
);
151154
}
152155

0 commit comments

Comments
 (0)