Skip to content

Commit ab4f33f

Browse files
authored
feat(core/eventStreams): add event stream mixin for HttpProtocols, schema-serde (#1671)
* feat(core/schema): event stream initial messages * wip: initial message testing * feat(core/eventStreams): test with eventStreamMarshaller * handle eventHeader and eventPayload traits * rename eventStreams submodule to event-streams
1 parent b7a4d81 commit ab4f33f

22 files changed

+907
-291
lines changed

.changeset/calm-trees-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/core": minor
3+
---
4+
5+
CBOR protocol error handling fallbacks

packages/core/event-streams.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Do not edit:
3+
* This is a compatibility redirect for contexts that do not understand package.json exports field.
4+
*/
5+
declare module "@smithy/core/event-streams" {
6+
export * from "@smithy/core/dist-types/submodules/event-streams/index.d";
7+
}

packages/core/event-streams.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
/**
3+
* Do not edit:
4+
* This is a compatibility redirect for contexts that do not understand package.json exports field.
5+
*/
6+
module.exports = require("./dist-cjs/submodules/event-streams/index.js");

packages/core/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@
6060
"node": "./dist-cjs/submodules/schema/index.js",
6161
"import": "./dist-es/submodules/schema/index.js",
6262
"require": "./dist-cjs/submodules/schema/index.js"
63+
},
64+
"./event-streams": {
65+
"types": "./dist-types/submodules/event-streams/index.d.ts",
66+
"module": "./dist-es/submodules/event-streams/index.js",
67+
"node": "./dist-cjs/submodules/event-streams/index.js",
68+
"import": "./dist-es/submodules/event-streams/index.js",
69+
"require": "./dist-cjs/submodules/event-streams/index.js"
6370
}
6471
},
6572
"author": {
@@ -95,6 +102,8 @@
95102
"files": [
96103
"./cbor.d.ts",
97104
"./cbor.js",
105+
"./event-streams.d.ts",
106+
"./event-streams.js",
98107
"./protocols.d.ts",
99108
"./protocols.js",
100109
"./schema.d.ts",
@@ -110,6 +119,7 @@
110119
"directory": "packages/core"
111120
},
112121
"devDependencies": {
122+
"@smithy/eventstream-serde-node": "workspace:^",
113123
"@types/node": "^18.11.9",
114124
"concurrently": "7.0.0",
115125
"downlevel-dts": "0.10.1",

packages/core/src/submodules/cbor/CborCodec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ export class CborShapeDeserializer implements ShapeDeserializer {
139139
return this.readValue(schema, data);
140140
}
141141

142-
private readValue(_schema: Schema, value: any): any {
142+
/**
143+
* Public because it's called by the protocol implementation to deserialize errors.
144+
* @internal
145+
*/
146+
public readValue(_schema: Schema, value: any): any {
143147
const ns = NormalizedSchema.of(_schema);
144148

145149
if (ns.isTimestampSchema() && typeof value === "number") {

packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { list, map, SCHEMA, struct } from "@smithy/core/schema";
1+
import { error, list, map, op, SCHEMA, struct, TypeRegistry } from "@smithy/core/schema";
22
import { HttpRequest, HttpResponse } from "@smithy/protocol-http";
3-
import type { SchemaRef } from "@smithy/types";
4-
import { describe, expect, test as it } from "vitest";
3+
import { ResponseMetadata, RetryableTrait, SchemaRef } from "@smithy/types";
4+
import { beforeEach, describe, expect, test as it } from "vitest";
55

66
import { cbor } from "./cbor";
77
import { dateToTag } from "./parseCborBody";
@@ -273,4 +273,92 @@ describe(SmithyRpcV2CborProtocol.name, () => {
273273
});
274274
}
275275
});
276+
277+
describe("error handling", () => {
278+
const protocol = new SmithyRpcV2CborProtocol({ defaultNamespace: "ns" });
279+
280+
const operation = op(
281+
"ns",
282+
"OperationWithModeledException",
283+
{},
284+
struct("ns", "Input", 0, [], []),
285+
struct("ns", "Output", 0, [], [])
286+
);
287+
288+
const errorResponse = new HttpResponse({
289+
statusCode: 400,
290+
headers: {},
291+
body: cbor.serialize({
292+
__type: "ns#ModeledException",
293+
modeledProperty: "oh no",
294+
}),
295+
});
296+
297+
const serdeContext = {};
298+
299+
class ServiceBaseException extends Error {
300+
public readonly $fault: "client" | "server" = "client";
301+
public $response?: HttpResponse;
302+
public $retryable?: RetryableTrait;
303+
public $metadata: ResponseMetadata = {
304+
httpStatusCode: 400,
305+
};
306+
}
307+
308+
class ModeledExceptionCtor extends ServiceBaseException {
309+
public modeledProperty: string = "";
310+
}
311+
312+
beforeEach(() => {
313+
TypeRegistry.for("ns").destroy();
314+
});
315+
316+
it("should throw the schema error ctor if one exists", async () => {
317+
// this is for modeled exceptions.
318+
319+
TypeRegistry.for("ns").register(
320+
"ns#ModeledException",
321+
error("ns", "ModeledException", 0, ["modeledProperty"], [0], ModeledExceptionCtor)
322+
);
323+
TypeRegistry.for("ns").register(
324+
"smithy.ts.sdk.synthetic.ns#BaseServiceException",
325+
error("smithy.ts.sdk.synthetic.ns", "BaseServiceException", 0, [], [], ServiceBaseException)
326+
);
327+
328+
try {
329+
await protocol.deserializeResponse(operation, serdeContext as any, errorResponse);
330+
} catch (e) {
331+
expect(e).toBeInstanceOf(ModeledExceptionCtor);
332+
expect((e as ModeledExceptionCtor).modeledProperty).toEqual("oh no");
333+
expect(e).toBeInstanceOf(ServiceBaseException);
334+
}
335+
expect.assertions(3);
336+
});
337+
338+
it("should throw a base error if available in the namespace, when no error schema is modeled", async () => {
339+
// this is the expected fallback case for all generated clients.
340+
341+
TypeRegistry.for("ns").register(
342+
"smithy.ts.sdk.synthetic.ns#BaseServiceException",
343+
error("smithy.ts.sdk.synthetic.ns", "BaseServiceException", 0, [], [], ServiceBaseException)
344+
);
345+
346+
try {
347+
await protocol.deserializeResponse(operation, serdeContext as any, errorResponse);
348+
} catch (e) {
349+
expect(e).toBeInstanceOf(ServiceBaseException);
350+
}
351+
expect.assertions(1);
352+
});
353+
354+
it("should fall back to a generic JS Error as a last resort", async () => {
355+
// this shouldn't happen, but in case the type registry is mutated incorrectly.
356+
try {
357+
await protocol.deserializeResponse(operation, serdeContext as any, errorResponse);
358+
} catch (e) {
359+
expect(e).toBeInstanceOf(Error);
360+
}
361+
expect.assertions(1);
362+
});
363+
});
276364
});

packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RpcProtocol } from "@smithy/core/protocols";
2-
import { deref, ErrorSchema, OperationSchema, TypeRegistry } from "@smithy/core/schema";
2+
import { deref, ErrorSchema, NormalizedSchema, OperationSchema, TypeRegistry } from "@smithy/core/schema";
33
import type {
44
EndpointBearer,
55
HandlerExecutionContext,
@@ -87,31 +87,54 @@ export class SmithyRpcV2CborProtocol extends RpcProtocol {
8787
dataObject: any,
8888
metadata: ResponseMetadata
8989
): Promise<never> {
90-
const error = loadSmithyRpcV2CborErrorCode(response, dataObject) ?? "Unknown";
90+
const errorName = loadSmithyRpcV2CborErrorCode(response, dataObject) ?? "Unknown";
9191

9292
let namespace = this.options.defaultNamespace;
93-
if (error.includes("#")) {
94-
[namespace] = error.split("#");
93+
if (errorName.includes("#")) {
94+
[namespace] = errorName.split("#");
9595
}
9696

97+
const errorMetadata = {
98+
$metadata: metadata,
99+
$response: response,
100+
$fault: response.statusCode <= 500 ? ("client" as const) : ("server" as const),
101+
};
102+
97103
const registry = TypeRegistry.for(namespace);
98-
const errorSchema: ErrorSchema = registry.getSchema(error) as ErrorSchema;
99104

100-
if (!errorSchema) {
101-
// TODO(schema) throw client base exception using the dataObject.
102-
throw new Error("schema not found for " + error);
105+
let errorSchema: ErrorSchema;
106+
try {
107+
errorSchema = registry.getSchema(errorName) as ErrorSchema;
108+
} catch (e) {
109+
if (dataObject.Message) {
110+
dataObject.message = dataObject.Message;
111+
}
112+
const baseExceptionSchema = TypeRegistry.for("smithy.ts.sdk.synthetic." + namespace).getBaseException();
113+
if (baseExceptionSchema) {
114+
const ErrorCtor = baseExceptionSchema.ctor;
115+
throw Object.assign(new ErrorCtor({ name: errorName }), errorMetadata, dataObject);
116+
}
117+
throw Object.assign(new Error(errorName), errorMetadata, dataObject);
103118
}
104119

120+
const ns = NormalizedSchema.of(errorSchema);
105121
const message = dataObject.message ?? dataObject.Message ?? "Unknown";
106122
const exception = new errorSchema.ctor(message);
107-
Object.assign(exception, {
108-
$metadata: metadata,
109-
$response: response,
110-
message,
111-
...dataObject,
112-
});
113123

114-
throw exception;
124+
const output = {} as any;
125+
for (const [name, member] of ns.structIterator()) {
126+
output[name] = this.deserializer.readValue(member, dataObject[name]);
127+
}
128+
129+
throw Object.assign(
130+
exception,
131+
errorMetadata,
132+
{
133+
$fault: ns.getMergedTraits().error,
134+
message,
135+
},
136+
output
137+
);
115138
}
116139

117140
protected getDefaultContentType(): string {

0 commit comments

Comments
 (0)