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
16 changes: 15 additions & 1 deletion clients/client-s3/test/e2e/S3.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import "@aws-sdk/signature-v4-crt";

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

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

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

Expand Down Expand Up @@ -246,5 +246,19 @@ describe("@aws-sdk/client-s3", () => {
expect(result.$metadata.httpStatusCode).toEqual(200);
expect(result.Contents).toBeInstanceOf(Array);
});

describe("error handling", () => {
it("should decorate exceptions with unmodeled error fields", async () => {
const error = await client
.abortMultipartUpload({
Bucket,
Key: "nonexistent-key",
UploadId: "uploadId",
})
.catch((e) => e);

expect((error as any).UploadId).toEqual("uploadId");
});
});
});
}, 60_000);
32 changes: 28 additions & 4 deletions packages/core/src/submodules/protocols/ProtocolLib.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { NormalizedSchema, TypeRegistry } from "@smithy/core/schema";
import { decorateServiceException, ServiceException as SDKBaseServiceException } from "@smithy/smithy-client";
import type { HttpResponse as IHttpResponse, MetadataBearer, ResponseMetadata, StaticErrorSchema } from "@smithy/types";

/**
* @internal
*/
type ErrorMetadataBearer = MetadataBearer & {
$response: IHttpResponse;
// $response is set by the deserializer middleware, not Protocol.
$fault: "client" | "server";
};

Expand All @@ -15,6 +16,8 @@ type ErrorMetadataBearer = MetadataBearer & {
* @internal
*/
export class ProtocolLib {
public constructor(private queryCompat = false) {}

/**
* This is only for REST protocols.
*
Expand Down Expand Up @@ -74,7 +77,6 @@ export class ProtocolLib {

const errorMetadata: ErrorMetadataBearer = {
$metadata: metadata,
$response: response,
$fault: response.statusCode < 500 ? ("client" as const) : ("server" as const),
};

Expand All @@ -90,10 +92,32 @@ export class ProtocolLib {
const baseExceptionSchema = synthetic.getBaseException();
if (baseExceptionSchema) {
const ErrorCtor = synthetic.getErrorCtor(baseExceptionSchema) ?? Error;
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
throw this.decorateServiceException(
Object.assign(new ErrorCtor({ name: errorName }), errorMetadata),
dataObject
);
}
throw this.decorateServiceException(Object.assign(new Error(errorName), errorMetadata), dataObject);
}
}

/**
* Assigns additions onto exception if not already present.
*/
public decorateServiceException<E extends SDKBaseServiceException>(
exception: E,
additions: Record<string, any> = {}
): E {
if (this.queryCompat) {
const msg = (exception as any).Message ?? additions.Message;
const error = decorateServiceException(exception, additions);
if (msg) {
(error as any).Message = msg;
(error as any).message = msg;
}
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
return error;
}
return decorateServiceException(exception, additions);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HttpResponse } from "@smithy/protocol-http";
import type { NumericSchema, StringSchema } from "@smithy/types";
import { describe, expect, test as it } from "vitest";

import { context } from "../test-schema.spec";
import { AwsSmithyRpcV2CborProtocol } from "./AwsSmithyRpcV2CborProtocol";

describe(AwsSmithyRpcV2CborProtocol.name, () => {
Expand Down Expand Up @@ -56,16 +57,8 @@ describe(AwsSmithyRpcV2CborProtocol.name, () => {
requestId: undefined,
});

expect(error.$response).toEqual(
new HttpResponse({
body,
headers: {
"x-amzn-query-error": "MyQueryError;Client",
},
reason: undefined,
statusCode: 400,
})
);
// set by deserializer middleware, not Protocol.
expect(error.$response).toEqual(undefined);

expect(error.Code).toEqual(MyQueryError.name);
expect(error.Error.Code).toEqual(MyQueryError.name);
Expand All @@ -91,4 +84,41 @@ describe(AwsSmithyRpcV2CborProtocol.name, () => {
Code: "MyQueryError",
});
});

it("decorates service exceptions with unmodeled fields", async () => {
const httpResponse = new HttpResponse({
statusCode: 400,
headers: {},
body: cbor.serialize({
UnmodeledField: "Oh no",
}),
});

const protocol = new AwsSmithyRpcV2CborProtocol({
defaultNamespace: "",
});

const output = await protocol
.deserializeResponse(
{
namespace: "ns",
name: "Empty",
traits: 0,
input: "unit" as const,
output: [3, "ns", "EmptyOutput", 0, [], []],
},
context,
httpResponse
)
.catch((e) => {
return e;
});

expect(output).toMatchObject({
UnmodeledField: "Oh no",
$metadata: {
httpStatusCode: 400,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ProtocolLib } from "../ProtocolLib";
*/
export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
private readonly awsQueryCompatible: boolean;
private readonly mixin = new ProtocolLib();
private readonly mixin: ProtocolLib;

public constructor({
defaultNamespace,
Expand All @@ -30,6 +30,7 @@ export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
}) {
super({ defaultNamespace });
this.awsQueryCompatible = !!awsQueryCompatible;
this.mixin = new ProtocolLib(this.awsQueryCompatible);
}

/**
Expand Down Expand Up @@ -84,14 +85,17 @@ export class AwsSmithyRpcV2CborProtocol extends SmithyRpcV2CborProtocol {
this.mixin.queryCompatOutput(dataObject, output);
}

throw Object.assign(
exception,
errorMetadata,
{
$fault: ns.getMergedTraits().error,
message,
},
output
throw this.mixin.decorateServiceException(
Object.assign(
exception,
errorMetadata,
{
$fault: ns.getMergedTraits().error,
message,
},
output
),
dataObject
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,40 @@ describe(AwsJson1_1Protocol, () => {
},
});
});

it("decorates service exceptions with unmodeled fields", async () => {
const httpResponse = new HttpResponse({
statusCode: 400,
headers: {},
body: Buffer.from(`{"UnmodeledField":"Oh no"}`),
});

const protocol = new AwsJson1_1Protocol({
defaultNamespace: "",
serviceTarget: "JsonRpc11",
});

const output = await protocol
.deserializeResponse(
{
namespace: "ns",
name: "Empty",
traits: 0,
input: "unit" as const,
output: [3, "ns", "EmptyOutput", 0, [], []],
},
context,
httpResponse
)
.catch((e) => {
return e;
});

expect(output).toMatchObject({
UnmodeledField: "Oh no",
$metadata: {
httpStatusCode: 400,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,8 @@ describe(AwsJsonRpcProtocol.name, () => {
requestId: undefined,
});

expect(error.$response).toEqual(
new HttpResponse({
body,
headers: {
"x-amzn-query-error": "MyQueryError;Client",
},
reason: undefined,
statusCode: 400,
})
);
// set by deserializer middleware, not protocol
expect(error.$response).toEqual(undefined);

expect(error.Code).toEqual(MyQueryError.name);
expect(error.Error.Code).toEqual(MyQueryError.name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
protected deserializer: ShapeDeserializer<string | Uint8Array>;
protected serviceTarget: string;
private readonly codec: JsonCodec;
private readonly mixin = new ProtocolLib();
private readonly mixin: ProtocolLib;
private readonly awsQueryCompatible: boolean;

protected constructor({
Expand All @@ -51,6 +51,7 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
this.serializer = this.codec.createSerializer();
this.deserializer = this.codec.createDeserializer();
this.awsQueryCompatible = !!awsQueryCompatible;
this.mixin = new ProtocolLib(this.awsQueryCompatible);
}

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

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

/**
* @override
*/
protected async handleError(
operationSchema: OperationSchema,
context: HandlerExecutionContext & SerdeFunctions,
Expand Down Expand Up @@ -120,14 +124,17 @@ export abstract class AwsJsonRpcProtocol extends RpcProtocol {
this.mixin.queryCompatOutput(dataObject, output);
}

throw Object.assign(
exception,
errorMetadata,
{
$fault: ns.getMergedTraits().error,
message,
},
output
throw this.mixin.decorateServiceException(
Object.assign(
exception,
errorMetadata,
{
$fault: ns.getMergedTraits().error,
message,
},
output
),
dataObject
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -410,5 +410,40 @@ describe(AwsRestJsonProtocol.name, () => {
payload: null,
});
});

it("decorates service exceptions with unmodeled fields", async () => {
const httpResponse = new HttpResponse({
statusCode: 400,
headers: {},
body: Buffer.from(`{"UnmodeledField":"Oh no"}`),
});

const protocol = new AwsRestJsonProtocol({
defaultNamespace: "",
});

const output = await protocol
.deserializeResponse(
{
namespace: "ns",
name: "Empty",
traits: 0,
input: "unit" as const,
output: [3, "ns", "EmptyOutput", 0, [], []],
},
context,
httpResponse
)
.catch((e) => {
return e;
});

expect(output).toMatchObject({
UnmodeledField: "Oh no",
$metadata: {
httpStatusCode: 400,
},
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,17 @@ export class AwsRestJsonProtocol extends HttpBindingProtocol {
output[name] = this.codec.createDeserializer().readObject(member, dataObject[target]);
}

throw Object.assign(
exception,
errorMetadata,
{
$fault: ns.getMergedTraits().error,
message,
},
output
throw this.mixin.decorateServiceException(
Object.assign(
exception,
errorMetadata,
{
$fault: ns.getMergedTraits().error,
message,
},
output
),
dataObject
);
}

Expand Down
Loading
Loading