Skip to content

Commit 44a72fe

Browse files
AllanZhengYPChase Coalwell
authored andcommitted
fix response payload and incorrectly parsing error response (smithy-lang#63)
* fix when response payload is not structured string * add support for parsing body in either dispatcher or deser For some protocols, error type flag exists in error response body, then we need to collect response stream to JS object and parse the error type; For other protocols, error type flag doesn't exist in error response body, then we don't need to collect the response stream in error dispatcher. Instead, we can treat the error like normal response. So that error shape supports the same traits as normal responses like streaming, payload etc. This is done by add a new flag in Protocol generator-- isErrorCodeInBody. When it return true, it means error type flag exists in error response body, then body is parsed in errors dispatcher, and each error deser only need to deal with parsed response body in JS object format. When it returns false, it means error type can be inferred without touching response body, then error deser can access the error response intact. * address feedbacks: fix indentation; readResponseBody; add docs * add response body string to unknown error message If incoming error response is unknow error, we only make best effort to infer the error type. This change puts error response body to error message so that users have the information they need. This change is necessary for Node because when error response is unknown, the body stream will never be consumed, which would use a lot of resources before stream is flushed.
1 parent 0fa51ff commit 44a72fe

File tree

3 files changed

+128
-41
lines changed

3 files changed

+128
-41
lines changed

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

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ public void generateSharedComponents(GenerationContext context) {
119119
generateDocumentBodyShapeSerializers(context, serializingDocumentShapes);
120120
generateDocumentBodyShapeDeserializers(context, deserializingDocumentShapes);
121121
HttpProtocolGeneratorUtils.generateMetadataDeserializer(context, getApplicationProtocol().getResponseType());
122+
HttpProtocolGeneratorUtils.generateCollectBody(context);
123+
HttpProtocolGeneratorUtils.generateCollectBodyString(context);
122124
}
123125

124126
/**
@@ -593,7 +595,7 @@ private void generateOperationDeserializer(
593595

594596
// Write out the error deserialization dispatcher.
595597
Set<StructureShape> errorShapes = HttpProtocolGeneratorUtils.generateErrorDispatcher(
596-
context, operation, responseType, this::writeErrorCodeParser);
598+
context, operation, responseType, this::writeErrorCodeParser, this.isErrorCodeInBody());
597599
deserializingErrorShapes.addAll(errorShapes);
598600
}
599601

@@ -605,11 +607,13 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
605607
Symbol errorSymbol = symbolProvider.toSymbol(error);
606608
String errorDeserMethodName = ProtocolGenerator.getDeserFunctionName(errorSymbol,
607609
context.getProtocolName()) + "Response";
610+
boolean isBodyParsed = this.isErrorCodeInBody();
608611

609612
writer.openBlock("const $L = async (\n"
610-
+ " output: any,\n"
613+
+ " $L: any,\n"
611614
+ " context: __SerdeContext\n"
612-
+ "): Promise<$T> => {", "};", errorDeserMethodName, errorSymbol, () -> {
615+
+ "): Promise<$T> => {", "};",
616+
errorDeserMethodName, isBodyParsed ? "parsedOutput" : "output", errorSymbol, () -> {
613617
writer.openBlock("const contents: $T = {", "};", errorSymbol, () -> {
614618
writer.write("__type: $S,", error.getId().getName());
615619
writer.write("$$fault: $S,", error.getTrait(ErrorTrait.class).get().getValue());
@@ -620,7 +624,7 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
620624
});
621625

622626
readHeaders(context, error, bindingIndex);
623-
List<HttpBinding> documentBindings = readResponseBody(context, error, bindingIndex);
627+
List<HttpBinding> documentBindings = readErrorResponseBody(context, error, bindingIndex, isBodyParsed);
624628
// Track all shapes bound to the document so their deserializers may be generated.
625629
documentBindings.forEach(binding -> {
626630
Shape target = model.expectShape(binding.getMember().getTarget());
@@ -632,6 +636,23 @@ private void generateErrorDeserializer(GenerationContext context, StructureShape
632636
writer.write("");
633637
}
634638

639+
private List<HttpBinding> readErrorResponseBody(
640+
GenerationContext context,
641+
Shape error,
642+
HttpBindingIndex bindingIndex,
643+
boolean isBodyParsed
644+
) {
645+
TypeScriptWriter writer = context.getWriter();
646+
if (isBodyParsed) {
647+
// Body is already parsed in error dispatcher, simply assign body to data.
648+
writer.write("const data: any = output.body;");
649+
return ListUtils.of();
650+
} else {
651+
// Deserialize response body just like in normal response.
652+
return readResponseBody(context, error, bindingIndex);
653+
}
654+
}
655+
635656
private void readHeaders(
636657
GenerationContext context,
637658
Shape operationOrError,
@@ -690,42 +711,44 @@ private List<HttpBinding> readResponseBody(
690711
documentBindings.sort(Comparator.comparing(HttpBinding::getMemberName));
691712
List<HttpBinding> payloadBindings = bindingIndex.getResponseBindings(operationOrError, Location.PAYLOAD);
692713

714+
OperationIndex operationIndex = context.getModel().getKnowledge(OperationIndex.class);
715+
StructureShape operationOutputOrError = operationOrError.asStructureShape()
716+
.orElseGet(() -> operationIndex.getOutput(operationOrError).orElse(null));
717+
boolean hasStreamingComponent = Optional.ofNullable(operationOutputOrError)
718+
.map(structure -> structure.getAllMembers().values().stream()
719+
.anyMatch(memberShape -> memberShape.hasTrait(StreamingTrait.class)))
720+
.orElse(false);
721+
693722
if (!documentBindings.isEmpty()) {
694-
readReponseBodyData(context, operationOrError);
723+
// If response has document binding, the body can be parsed to JavaScript object.
724+
writer.write("const data: any = await parseBody(output.body, context);");
695725
deserializeOutputDocument(context, operationOrError, documentBindings);
696726
return documentBindings;
697727
}
698728
if (!payloadBindings.isEmpty()) {
699-
readReponseBodyData(context, operationOrError);
700729
// There can only be one payload binding.
701730
HttpBinding binding = payloadBindings.get(0);
702731
Shape target = context.getModel().expectShape(binding.getMember().getTarget());
732+
if (hasStreamingComponent) {
733+
// If payload is streaming, return raw low-level stream directly.
734+
writer.write("const data: any = output.body;");
735+
} else if (target instanceof BlobShape) {
736+
// If payload is blob, only need to collect stream to binary data(Uint8Array).
737+
writer.write("const data: any = await collectBody(output.body, context);");
738+
} else if (target instanceof StructureShape || target instanceof UnionShape) {
739+
// If body is Structure or Union, they we need to parse the string into JavaScript object.
740+
writer.write("const data: any = await parseBody(output.body, context);");
741+
} else {
742+
// If payload is string, we need to collect body and encode binary to string.
743+
writer.write("const data: any = await collectBodyString(output.body, context);");
744+
}
703745
writer.write("contents.$L = $L;", binding.getMemberName(), getOutputValue(context,
704746
Location.PAYLOAD, "data", binding.getMember(), target));
705747
return payloadBindings;
706748
}
707749
return ListUtils.of();
708750
}
709751

710-
private void readReponseBodyData(GenerationContext context, Shape operationOrError) {
711-
TypeScriptWriter writer = context.getWriter();
712-
// Prepare response body for deserializing.
713-
OperationIndex operationIndex = context.getModel().getKnowledge(OperationIndex.class);
714-
StructureShape operationOutputOrError = operationOrError.asStructureShape()
715-
.orElseGet(() -> operationIndex.getOutput(operationOrError).orElse(null));
716-
boolean hasStreamingComponent = Optional.ofNullable(operationOutputOrError)
717-
.map(structure -> structure.getAllMembers().values().stream()
718-
.anyMatch(memberShape -> memberShape.hasTrait(StreamingTrait.class)))
719-
.orElse(false);
720-
if (hasStreamingComponent) {
721-
// For operations with streaming output or errors with streaming body we keep the body intact.
722-
writer.write("const data: any = output.body;");
723-
} else {
724-
// Otherwise, we collect the response body to structured object with parseBody().
725-
writer.write("const data: any = await parseBody(output.body, context);");
726-
}
727-
}
728-
729752
/**
730753
* Given context and a source of data, generate an output value provider for the
731754
* shape. This may use native types (like generating a Date for timestamps,)
@@ -906,6 +929,14 @@ private String getNumberOutputParam(Location bindingType, String dataSource, Sha
906929
*/
907930
protected abstract void writeErrorCodeParser(GenerationContext context);
908931

932+
/**
933+
* A boolean indicates whether body is collected and parsed in error code parser.
934+
* If so, each error shape deserializer should not parse body again.
935+
*
936+
* @return returns whether the error code exists in response body
937+
*/
938+
protected abstract boolean isErrorCodeInBody();
939+
909940
/**
910941
* Writes the code needed to deserialize the output document of a response.
911942
*

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

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,44 @@ static void generateMetadataDeserializer(GenerationContext context, SymbolRefere
108108
writer.write("");
109109
}
110110

111+
/**
112+
* Writes a response body stream collector. This function converts the low-level response body stream to
113+
* Uint8Array binary data.
114+
*
115+
* @param context The generation context.
116+
*/
117+
static void generateCollectBody(GenerationContext context) {
118+
TypeScriptWriter writer = context.getWriter();
119+
120+
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
121+
writer.write("// Collect low-level response body stream to Uint8Array.");
122+
writer.openBlock("const collectBody = (streamBody: any, context: __SerdeContext): Promise<Uint8Array> => {",
123+
"};", () -> {
124+
writer.write("return context.streamCollector(streamBody) || new Uint8Array();");
125+
});
126+
127+
writer.write("");
128+
}
129+
130+
/**
131+
* Writes a function converting the low-level response body stream to utf-8 encoded string. It depends on
132+
* response body stream collector{@link #generateCollectBody(GenerationContext)}.
133+
*
134+
* @param context The generation context
135+
*/
136+
static void generateCollectBodyString(GenerationContext context) {
137+
TypeScriptWriter writer = context.getWriter();
138+
139+
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
140+
writer.write("// Encode Uint8Array data into string with utf-8.");
141+
writer.openBlock("const collectBodyString = (streamBody: any, context: __SerdeContext): Promise<string> => {",
142+
"};", () -> {
143+
writer.write("return collectBody(streamBody, context).then(body => context.utf8Encoder(body));");
144+
});
145+
146+
writer.write("");
147+
}
148+
111149
/**
112150
* Writes a function used to dispatch to the proper error deserializer
113151
* for each error that the operation can return. The generated function
@@ -118,13 +156,15 @@ static void generateMetadataDeserializer(GenerationContext context, SymbolRefere
118156
* @param operation The operation to generate for.
119157
* @param responseType The response type for the HTTP protocol.
120158
* @param errorCodeGenerator A consumer
159+
* @param shouldParseErrorBody Flag indicating whether need to parse response body
121160
* @return A set of all error structure shapes for the operation that were dispatched to.
122161
*/
123162
static Set<StructureShape> generateErrorDispatcher(
124163
GenerationContext context,
125164
OperationShape operation,
126165
SymbolReference responseType,
127-
Consumer<GenerationContext> errorCodeGenerator
166+
Consumer<GenerationContext> errorCodeGenerator,
167+
boolean shouldParseErrorBody
128168
) {
129169
TypeScriptWriter writer = context.getWriter();
130170
SymbolProvider symbolProvider = context.getSymbolProvider();
@@ -138,15 +178,15 @@ static Set<StructureShape> generateErrorDispatcher(
138178
+ " output: $T,\n"
139179
+ " context: __SerdeContext,\n"
140180
+ "): Promise<$T> {", "}", errorMethodName, responseType, outputType, () -> {
141-
writer.write("const data: any = await parseBody(output.body, context);");
142-
// We only consume the parsedOutput if we're dispatching, so only generate if we will.
143-
if (!operation.getErrors().isEmpty()) {
144-
// Create a holding object since we have already parsed the body, but retain the rest of the output.
145-
writer.openBlock("const parsedOutput: any = {", "};", () -> {
146-
writer.write("...output,");
147-
writer.write("body: data,");
148-
});
149-
}
181+
// Prepare error response for parsing error code. If error code needs to be parsed from response body
182+
// then we collect body and parse it to JS object, otherwise leave the response body as is.
183+
writer.openBlock(
184+
"const $L: any = {", "};", shouldParseErrorBody ? "parsedOutput" : "errorOutput",
185+
() -> {
186+
writer.write("...output,");
187+
writer.write("body: $L,",
188+
shouldParseErrorBody ? "await parseBody(output.body, context)" : "output.body");
189+
});
150190

151191
// Error responses must be at least SmithyException and MetadataBearer implementations.
152192
writer.addImport("SmithyException", "__SmithyException",
@@ -167,17 +207,19 @@ static Set<StructureShape> generateErrorDispatcher(
167207
context.getProtocolName()) + "Response";
168208
writer.openBlock("case $S:\ncase $S:", " break;", errorId.getName(), errorId.toString(), () -> {
169209
// Dispatch to the error deserialization function.
170-
writer.write("response = await $L(parsedOutput, context);", errorDeserMethodName);
210+
writer.write("response = await $L($L, context);",
211+
errorDeserMethodName, shouldParseErrorBody ? "parsedOutput" : "errorOutput");
171212
});
172213
});
173214

174215
// Build a generic error the best we can for ones we don't know about.
175216
writer.write("default:").indent()
176217
.write("errorCode = errorCode || \"UnknownError\";")
177-
.openBlock("response = {", "};", () -> {
218+
.openBlock("response = {", "} as any;", () -> {
178219
writer.write("__type: `$L#$${errorCode}`,", operation.getId().getNamespace());
179220
writer.write("$$fault: \"client\",");
180221
writer.write("$$metadata: deserializeMetadata(output),");
222+
writer.write("message: await collectBodyString(output.body, context)");
181223
}).dedent();
182224
});
183225
writer.write("return Promise.reject(Object.assign(new Error(response.__type), response));");

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ private void generateOperationDeserializer(GenerationContext context, OperationS
254254

255255
// Write out the error deserialization dispatcher.
256256
Set<StructureShape> errorShapes = HttpProtocolGeneratorUtils.generateErrorDispatcher(
257-
context, operation, responseType, this::writeErrorCodeParser);
257+
context, operation, responseType, this::writeErrorCodeParser, this.isErrorCodeInBody());
258258
deserializingErrorShapes.addAll(errorShapes);
259259
}
260260

@@ -311,10 +311,13 @@ private void readResponseBody(GenerationContext context, OperationShape operatio
311311
* Writes the code that loads an {@code errorCode} String with the content used
312312
* to dispatch errors to specific serializers.
313313
*
314-
* <p>Three variables will be in scope:
314+
* <p>Two variables will be in scope:
315315
* <ul>
316-
* <li>{@code output}: a value of the HttpResponse type.</li>
317-
* <li>{@code data}: the contents of the response body.</li>
316+
* <li>{@code errorOutput} or {@code parsedOutput}: a value of the HttpResponse type.
317+
* {@code errorOutput} is a raw HttpResponse whereas {@code parsedOutput} is a HttpResponse type with
318+
* body parsed to JavaScript object.
319+
* The actual value available is determined by {@link #isErrorCodeInBody}
320+
* </li>
318321
* <li>{@code context}: the SerdeContext.</li>
319322
* </ul>
320323
*
@@ -328,6 +331,17 @@ private void readResponseBody(GenerationContext context, OperationShape operatio
328331
*/
329332
protected abstract void writeErrorCodeParser(GenerationContext context);
330333

334+
/**
335+
* Indicates whether body is collected and parsed in error dispatcher.
336+
*
337+
* <p>If returns true, {@link #writeErrorCodeParser} will have {@code parsedOutput} in scope
338+
*
339+
* <P>If returns false, {@link #writeErrorCodeParser} will have {@code errorOutput} in scope
340+
*
341+
* @return returns whether the error code exists in response body
342+
*/
343+
protected abstract boolean isErrorCodeInBody();
344+
331345
/**
332346
* Writes the code needed to deserialize the output document of a response.
333347
*

0 commit comments

Comments
 (0)