Skip to content

Commit daf42da

Browse files
committed
Support serializing framework exceptions
Framework exceptions are ones that are not modeled by the service owner and will never be expected to have specific types generated by any client. They are not framework errors simply because they are ubiquitous, but because we have to be able to throw them regardless of the service model. They should represent the inability for the framework to process the request, as opposed to the framework refusing to process the request.
1 parent 1735105 commit daf42da

File tree

7 files changed

+159
-36
lines changed

7 files changed

+159
-36
lines changed

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CodegenVisitor.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ public Void serviceShape(ServiceShape shape) {
329329
context.withSymbolProvider(serverSymbolProvider);
330330
protocolGenerator.generateRequestDeserializers(serverContext);
331331
protocolGenerator.generateResponseSerializers(serverContext);
332+
protocolGenerator.generateFrameworkErrorSerializer(serverContext);
332333
writers.useShapeWriter(shape, serverSymbolProvider, w -> {
333334
protocolGenerator.generateHandlerFactory(serverContext.withWriter(w));
334335
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.typescript.codegen;
17+
18+
import software.amazon.smithy.model.Model;
19+
20+
public enum FrameworkErrorModel {
21+
22+
INSTANCE;
23+
24+
private final Model model = Model.assembler()
25+
.addImport(FrameworkErrorModel.class.getResource("framework-errors.smithy"))
26+
.assemble()
27+
.unwrap();
28+
29+
public Model getModel() {
30+
return model;
31+
}
32+
}

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServerGenerator.java

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ static void generateServiceHandler(SymbolProvider symbolProvider,
4949
writer.addImport("ServiceHandler", null, "@aws-smithy/server-common");
5050
writer.addImport("Mux", null, "@aws-smithy/server-common");
5151
writer.addImport("OperationSerializer", null, "@aws-smithy/server-common");
52+
writer.addImport("UnknownOperationException", null, "@aws-smithy/server-common");
53+
writer.addImport("InternalFailureException", null, "@aws-smithy/server-common");
54+
writer.addImport("SerializationException", null, "@aws-smithy/server-common");
55+
writer.addImport("SmithyFrameworkException", null, "@aws-smithy/server-common");
56+
writer.addImport("SerdeContext", null, "@aws-sdk/types");
5257
writer.addImport("NodeHttpHandler", null, "@aws-sdk/node-http-handler");
5358
writer.addImport("streamCollector", null, "@aws-sdk/node-http-handler");
5459
writer.addImport("fromBase64", null, "@aws-sdk/util-base64-node");
@@ -68,6 +73,8 @@ static void generateServiceHandler(SymbolProvider symbolProvider,
6873
writer.write("private mux: Mux<$S, $T>;", serviceShape.getId().getName(), operationsType);
6974
writer.write("private serializerFactory: <T extends $T>(operation: T) => "
7075
+ "OperationSerializer<$T, T, SmithyException>;", operationsType, serviceSymbol);
76+
writer.write("private serializeFrameworkException: (e: SmithyFrameworkException, "
77+
+ "ctx: Omit<SerdeContext, 'endpoint'>) => Promise<HttpResponse>;");
7178
writer.openBlock("private serdeContextBase = {", "};", () -> {
7279
writer.write("base64Encoder: toBase64,");
7380
writer.write("base64Decoder: fromBase64,");
@@ -87,51 +94,66 @@ static void generateServiceHandler(SymbolProvider symbolProvider,
8794
writer.write("operation in $T that ", serviceSymbol);
8895
writer.writeInline(" ")
8996
.write("handles deserialization of requests and serialization of responses");
97+
writer.write("@param serializeFrameworkException A function that can serialize "
98+
+ "{@link SmithyFrameworkException}s");
9099
});
91-
writer.openBlock("constructor(service: $1T, "
92-
+ "mux: Mux<$3S, $2T>, "
93-
+ "serializerFactory: <T extends $2T>(op: T) => "
94-
+ "OperationSerializer<$1T, T, SmithyException>) {", "}",
95-
serviceSymbol, operationsType, serviceShape.getId().getName(), () -> {
96-
writer.write("this.service = service;");
97-
writer.write("this.mux = mux;");
98-
writer.write("this.serializerFactory = serializerFactory;");
100+
writer.openBlock("constructor(", ") {", () -> {
101+
writer.write("service: $T,", serviceSymbol);
102+
writer.write("mux: Mux<$S, $T>,", serviceShape.getId().getName(), operationsType);
103+
writer.write("serializerFactory:<T extends $T>(op: T) => OperationSerializer<$T, T, SmithyException>,",
104+
operationsType, serviceSymbol);
105+
writer.write("serializeFrameworkException: (e: SmithyFrameworkException, ctx: Omit<SerdeContext, "
106+
+ "'endpoint'>) => Promise<HttpResponse>");
99107
});
108+
writer.indent();
109+
writer.write("this.service = service;");
110+
writer.write("this.mux = mux;");
111+
writer.write("this.serializerFactory = serializerFactory;");
112+
writer.write("this.serializeFrameworkException = serializeFrameworkException;");
113+
writer.closeBlock("}");
100114
writer.openBlock("async handle(request: HttpRequest): Promise<HttpResponse> {", "}", () -> {
101115
writer.write("const target = this.mux.match(request);");
102116
writer.openBlock("if (target === undefined) {", "}", () -> {
103-
writer.write("throw new Error(`Could not match any operation to $${request.method} "
104-
+ "$${request.path} $${JSON.stringify(request.query)}`);");
117+
writer.write("return serializeFrameworkException(new UnknownOperationException(), "
118+
+ "this.serdeContextBase);");
105119
});
106120
writer.openBlock("switch (target.operation) {", "}", () -> {
107121
for (OperationShape operation : operations) {
108-
generateHandlerCase(writer, serviceSymbol, operation, symbolProvider.toSymbol(operation));
122+
generateHandlerCase(writer, operation, symbolProvider.toSymbol(operation));
109123
}
110124
});
111125
});
112126
});
113127
}
114128

115129
private static void generateHandlerCase(TypeScriptWriter writer,
116-
Symbol serviceSymbol,
117130
Shape operationShape,
118131
Symbol operationSymbol) {
119132
String opName = operationShape.getId().getName();
120133
writer.openBlock("case $S : {", "}", opName, () -> {
121134
writer.write("let serializer = this.serializerFactory($S);", opName);
122-
writer.openBlock("try {", "} catch(error: unknown) {", () -> {
123-
writer.openBlock("let input = await serializer.deserialize(request, {", "});", () -> {
135+
writer.write("let input;");
136+
writer.openBlock("try {", "} catch (error: unknown) {", () -> {
137+
writer.openBlock("input = await serializer.deserialize(request, {", "});", () -> {
124138
writer.write("endpoint: () => Promise.resolve(request), ...this.serdeContextBase");
125139
});
140+
});
141+
writer.indent();
142+
writer.write("return this.serializeFrameworkException(new SerializationException(), "
143+
+ "this.serdeContextBase);");
144+
writer.closeBlock("}");
145+
writer.openBlock("try {", "} catch(error: unknown) {", () -> {
126146
writer.write("let output = this.service.$L(input, request);", operationSymbol.getName());
127147
writer.write("return serializer.serialize(output, this.serdeContextBase);");
128148
});
129-
writer.openBlock("", "}", () -> {
130-
writer.openBlock("if (serializer.isOperationError(error)) {", "}", () -> {
131-
writer.write("return serializer.serializeError(error, this.serdeContextBase);");
132-
});
133-
writer.write("throw error;");
149+
writer.indent();
150+
writer.openBlock("if (serializer.isOperationError(error)) {", "}", () -> {
151+
writer.write("return serializer.serializeError(error, this.serdeContextBase);");
134152
});
153+
writer.write("console.log('Received an unexpected error', error);");
154+
writer.write("return this.serializeFrameworkException(new InternalFailureException(), "
155+
+ "this.serdeContextBase);");
156+
writer.closeBlock("}");
135157
});
136158
}
137159

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpBindingProtocolGenerator.java

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
6969
import software.amazon.smithy.typescript.codegen.ApplicationProtocol;
7070
import software.amazon.smithy.typescript.codegen.CodegenUtils;
71+
import software.amazon.smithy.typescript.codegen.FrameworkErrorModel;
7172
import software.amazon.smithy.typescript.codegen.TypeScriptDependency;
7273
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
7374
import software.amazon.smithy.utils.ListUtils;
@@ -216,6 +217,37 @@ public void generateResponseSerializers(GenerationContext context) {
216217
}
217218
}
218219

220+
@Override
221+
public void generateFrameworkErrorSerializer(GenerationContext inputContext) {
222+
final GenerationContext context = inputContext.copy();
223+
context.setModel(FrameworkErrorModel.INSTANCE.getModel());
224+
225+
SymbolReference responseType = getApplicationProtocol().getResponseType();
226+
HttpBindingIndex bindingIndex = HttpBindingIndex.of(context.getModel());
227+
TypeScriptWriter writer = context.getWriter();
228+
229+
writer.addImport("SmithyFrameworkException", "__SmithyFrameworkException", "@aws-smithy/server-common");
230+
writer.addUseImports(responseType);
231+
232+
writer.openBlock("export const serializeFrameworkException = async(\n"
233+
+ " input: __SmithyFrameworkException,\n"
234+
+ " ctx: Omit<__SerdeContext, 'endpoint'>\n"
235+
+ "): Promise<$T> => {", "}", responseType, () -> {
236+
237+
writeEmptyEndpoint(context);
238+
239+
writer.openBlock("switch (input.name) {", "}", () -> {
240+
for (final Shape shape : context.getModel().getShapesWithTrait(HttpErrorTrait.class)) {
241+
StructureShape errorShape = shape.asStructureShape().orElseThrow(IllegalArgumentException::new);
242+
writer.openBlock("case $S: {", "}", errorShape.getId().getName(), () -> {
243+
generateErrorSerializationImplementation(context, errorShape, responseType, bindingIndex);
244+
});
245+
}
246+
});
247+
});
248+
writer.write("");
249+
}
250+
219251
private void generateMux(GenerationContext context) {
220252
TopDownIndex topDownIndex = TopDownIndex.of(context.getModel());
221253
TypeScriptWriter writer = context.getWriter();
@@ -295,6 +327,9 @@ public void generateHandlerFactory(GenerationContext context) {
295327
Set<OperationShape> operations = index.getContainedOperations(context.getService());
296328
SymbolProvider symbolProvider = context.getSymbolProvider();
297329

330+
writer.addImport("serializeFrameworkException", null,
331+
"./protocols/" + ProtocolGenerator.getSanitizedName(getName()));
332+
298333
Symbol serviceSymbol = symbolProvider.toSymbol(context.getService());
299334
Symbol handlerSymbol = serviceSymbol.expectProperty("handler", Symbol.class);
300335
Symbol operationsSymbol = serviceSymbol.expectProperty("operations", Symbol.class);
@@ -311,7 +346,7 @@ public void generateHandlerFactory(GenerationContext context) {
311346
.forEach(writeOperationCase(writer, symbolProvider));
312347
});
313348
});
314-
writer.write("return new $T(service, mux, serFn);", handlerSymbol);
349+
writer.write("return new $T(service, mux, serFn, serializeFrameworkException);", handlerSymbol);
315350
});
316351
}
317352

@@ -402,25 +437,33 @@ private void generateErrorSerializer(GenerationContext context, StructureShape e
402437
+ " ctx: Omit<__SerdeContext, 'endpoint'>\n"
403438
+ "): Promise<$T> => {", "}", methodName, symbol, responseType, () -> {
404439
writeEmptyEndpoint(context);
405-
writeErrorStatusCode(context, error);
406-
writeResponseHeaders(context, error, bindingIndex, () -> writeDefaultErrorHeaders(context, error));
440+
generateErrorSerializationImplementation(context, error, responseType, bindingIndex);
441+
});
442+
writer.write("");
443+
}
407444

408-
List<HttpBinding> bodyBindings = writeResponseBody(context, error, bindingIndex);
409-
if (!bodyBindings.isEmpty()) {
410-
// Track all shapes bound to the body so their serializers may be generated.
411-
bodyBindings.stream()
412-
.map(HttpBinding::getMember)
413-
.map(member -> context.getModel().expectShape(member.getTarget()))
414-
.forEach(serializingDocumentShapes::add);
415-
}
445+
private void generateErrorSerializationImplementation(GenerationContext context,
446+
StructureShape error,
447+
SymbolReference responseType,
448+
HttpBindingIndex bindingIndex) {
449+
TypeScriptWriter writer = context.getWriter();
450+
writeErrorStatusCode(context, error);
451+
writeResponseHeaders(context, error, bindingIndex, () -> writeDefaultErrorHeaders(context, error));
452+
453+
List<HttpBinding> bodyBindings = writeResponseBody(context, error, bindingIndex);
454+
if (!bodyBindings.isEmpty()) {
455+
// Track all shapes bound to the body so their serializers may be generated.
456+
bodyBindings.stream()
457+
.map(HttpBinding::getMember)
458+
.map(member -> context.getModel().expectShape(member.getTarget()))
459+
.forEach(serializingDocumentShapes::add);
460+
}
416461

417-
writer.openBlock("return new $T({", "});", responseType, () -> {
418-
writer.write("headers,");
419-
writer.write("body,");
420-
writer.write("statusCode,");
421-
});
462+
writer.openBlock("return new $T({", "});", responseType, () -> {
463+
writer.write("headers,");
464+
writer.write("body,");
465+
writer.write("statusCode,");
422466
});
423-
writer.write("");
424467
}
425468

426469
private void writeEmptyEndpoint(GenerationContext context) {

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/HttpRpcProtocolGenerator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ public void generateRequestSerializers(GenerationContext context) {
143143
}
144144
}
145145

146+
@Override
147+
public void generateFrameworkErrorSerializer(GenerationContext serverContext) {
148+
LOGGER.warning("Framework exception serialization is not currently supported for RPC protocols.");
149+
}
150+
146151
@Override
147152
public void generateRequestDeserializers(GenerationContext context) {
148153
LOGGER.warning("Request deserialization is not currently supported for RPC protocols.");

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/integration/ProtocolGenerator.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ default void generateSharedComponents(GenerationContext context) {
148148
*/
149149
void generateResponseSerializers(GenerationContext context);
150150

151+
/**
152+
* Generates the code used to serialize unmodeled errors for servers.
153+
*
154+
* @param serverContext Serialization context.
155+
*/
156+
void generateFrameworkErrorSerializer(GenerationContext serverContext);
157+
151158
/**
152159
* Generates the code used to determine the service and operation
153160
* targeted by a given request.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace smithy.framework
2+
3+
@error("server")
4+
@httpError(500)
5+
structure InternalFailure {}
6+
7+
@error("client")
8+
@httpError(404)
9+
structure UnknownOperationException {}
10+
11+
@error("client")
12+
@httpError(400)
13+
structure SerializationException {}

0 commit comments

Comments
 (0)