Skip to content

Commit 8cb9942

Browse files
authored
Merge pull request #655 from aws/varunkn/EventStreamModeledExceptions
Support modeled exceptions in event streams
2 parents f0b1a54 + bd96ba9 commit 8cb9942

File tree

9 files changed

+140
-71
lines changed

9 files changed

+140
-71
lines changed

codegen/src/main/java/software/amazon/awssdk/codegen/AddExceptionShapes.java

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616
package software.amazon.awssdk.codegen;
1717

1818
import java.util.HashMap;
19-
import java.util.List;
2019
import java.util.Map;
2120
import software.amazon.awssdk.codegen.internal.Utils;
2221
import software.amazon.awssdk.codegen.model.intermediate.OperationModel;
2322
import software.amazon.awssdk.codegen.model.intermediate.ShapeModel;
2423
import software.amazon.awssdk.codegen.model.intermediate.ShapeType;
25-
import software.amazon.awssdk.codegen.model.service.ErrorMap;
2624
import software.amazon.awssdk.codegen.model.service.ErrorTrait;
27-
import software.amazon.awssdk.codegen.model.service.Operation;
25+
import software.amazon.awssdk.codegen.model.service.Shape;
2826

2927
/**
3028
* Constructs the exception shapes for the intermediate model. Analyzes the operations in the
@@ -46,28 +44,21 @@ private Map<String, ShapeModel> constructExceptionShapes() {
4644
// Java shape models, to be constructed
4745
final Map<String, ShapeModel> javaShapes = new HashMap<String, ShapeModel>();
4846

49-
for (Map.Entry<String, Operation> entry : getServiceModel().getOperations().entrySet()) {
47+
for (Map.Entry<String, Shape> shape : getServiceModel().getShapes().entrySet()) {
48+
if (shape.getValue().isException()) {
49+
String errorShapeName = shape.getKey();
50+
String javaClassName = getNamingStrategy().getExceptionName(errorShapeName);
5051

51-
Operation operation = entry.getValue();
52-
List<ErrorMap> operationErrors = operation.getErrors();
52+
ShapeModel exceptionShapeModel = generateShapeModel(javaClassName,
53+
errorShapeName);
5354

54-
if (operationErrors != null) {
55-
for (ErrorMap error : operationErrors) {
56-
57-
String errorShapeName = error.getShape();
58-
String javaClassName = getNamingStrategy().getExceptionName(errorShapeName);
59-
60-
ShapeModel exceptionShapeModel = generateShapeModel(javaClassName,
61-
errorShapeName);
62-
63-
exceptionShapeModel.setType(ShapeType.Exception.getValue());
64-
exceptionShapeModel.setErrorCode(getErrorCode(errorShapeName));
65-
if (exceptionShapeModel.getDocumentation() == null) {
66-
exceptionShapeModel.setDocumentation(error.getDocumentation());
67-
}
68-
69-
javaShapes.put(javaClassName, exceptionShapeModel);
55+
exceptionShapeModel.setType(ShapeType.Exception.getValue());
56+
exceptionShapeModel.setErrorCode(getErrorCode(errorShapeName));
57+
if (exceptionShapeModel.getDocumentation() == null) {
58+
exceptionShapeModel.setDocumentation(shape.getValue().getDocumentation());
7059
}
60+
61+
javaShapes.put(javaClassName, exceptionShapeModel);
7162
}
7263
}
7364

codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/JsonProtocolSpec.java

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@
2828
import java.util.Optional;
2929
import java.util.stream.Collectors;
3030
import javax.lang.model.element.Modifier;
31-
3231
import software.amazon.awssdk.awscore.eventstream.EventStreamAsyncResponseTransformer;
33-
import software.amazon.awssdk.awscore.eventstream.EventStreamExceptionJsonUnmarshaller;
3432
import software.amazon.awssdk.awscore.eventstream.EventStreamTaggedUnionJsonUnmarshaller;
3533
import software.amazon.awssdk.awscore.exception.AwsServiceException;
3634
import software.amazon.awssdk.awscore.internal.protocol.json.AwsJsonProtocol;
@@ -159,19 +157,6 @@ public CodeBlock responseHandler(IntermediateModel model, OperationModel opModel
159157
});
160158
builder.add(".defaultUnmarshaller((in) -> $T.UNKNOWN)\n"
161159
+ ".build());\n", eventStreamUtils.eventStreamBaseClass());
162-
163-
builder.add("\n\n$T<$T> exceptionHandler = $L.createResponseHandler(\n" +
164-
" new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false),\n" +
165-
" $T.builder()\n" +
166-
" .defaultUnmarshaller(x -> $T.populateDefaultException($T::builder, x))\n" +
167-
" .build());",
168-
HttpResponseHandler.class,
169-
WildcardTypeName.subtypeOf(Throwable.class),
170-
protocolFactory,
171-
EventStreamExceptionJsonUnmarshaller.class,
172-
EventStreamExceptionJsonUnmarshaller.class,
173-
baseExceptionClassName(model));
174-
175160
}
176161
return builder.build();
177162
}
@@ -229,7 +214,7 @@ public CodeBlock asyncExecutionHandler(OperationModel opModel) {
229214
CodeBlock.Builder builder = CodeBlock.builder();
230215
if (opModel.hasEventStreamOutput()) {
231216
builder.add("$T<$T, $T> asyncResponseTransformer = new $T<>(\n"
232-
+ "asyncResponseHandler, responseHandler, eventResponseHandler, exceptionHandler);\n",
217+
+ "asyncResponseHandler, responseHandler, eventResponseHandler, errorResponseHandler, serviceName());\n",
233218
ClassName.get(AsyncResponseTransformer.class),
234219
ClassName.get(SdkResponse.class),
235220
ClassName.get(Void.class),

codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-async-client-class.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
1212
import software.amazon.awssdk.awscore.client.handler.AwsAsyncClientHandler;
1313
import software.amazon.awssdk.awscore.eventstream.EventStreamAsyncResponseTransformer;
14-
import software.amazon.awssdk.awscore.eventstream.EventStreamExceptionJsonUnmarshaller;
1514
import software.amazon.awssdk.awscore.eventstream.EventStreamTaggedUnionJsonUnmarshaller;
1615
import software.amazon.awssdk.awscore.exception.AwsServiceException;
1716
import software.amazon.awssdk.awscore.internal.protocol.json.AwsJsonProtocol;
@@ -42,7 +41,6 @@
4241
import software.amazon.awssdk.services.json.model.GetWithoutRequiredMembersRequest;
4342
import software.amazon.awssdk.services.json.model.GetWithoutRequiredMembersResponse;
4443
import software.amazon.awssdk.services.json.model.InvalidInputException;
45-
import software.amazon.awssdk.services.json.model.JsonException;
4644
import software.amazon.awssdk.services.json.model.JsonRequest;
4745
import software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest;
4846
import software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse;
@@ -222,18 +220,9 @@ public CompletableFuture<Void> eventStreamOperation(EventStreamOperationRequest
222220
.addUnmarshaller("EventTwo", EventTwoUnmarshaller.getInstance())
223221
.defaultUnmarshaller((in) -> EventStream.UNKNOWN).build());
224222

225-
HttpResponseHandler<? extends Throwable> exceptionHandler = jsonProtocolFactory
226-
.createResponseHandler(
227-
new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false),
228-
EventStreamExceptionJsonUnmarshaller
229-
.builder()
230-
.defaultUnmarshaller(
231-
x -> EventStreamExceptionJsonUnmarshaller.populateDefaultException(
232-
JsonException::builder, x)).build());
233-
234223
HttpResponseHandler<AwsServiceException> errorResponseHandler = createErrorResponseHandler(jsonProtocolFactory);
235224
AsyncResponseTransformer<SdkResponse, Void> asyncResponseTransformer = new EventStreamAsyncResponseTransformer<>(
236-
asyncResponseHandler, responseHandler, eventResponseHandler, exceptionHandler);
225+
asyncResponseHandler, responseHandler, eventResponseHandler, errorResponseHandler, serviceName());
237226

238227
return clientHandler.execute(
239228
new ClientExecutionParams<EventStreamOperationRequest, SdkResponse>()

core/aws-core/src/main/java/software/amazon/awssdk/awscore/eventstream/EventStreamAsyncResponseTransformer.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package software.amazon.awssdk.awscore.eventstream;
1717

1818
import static java.util.Collections.singletonList;
19+
import static software.amazon.awssdk.core.http.HttpResponseHandler.X_AMZN_REQUEST_ID_HEADER;
1920
import static software.amazon.awssdk.utils.FunctionalUtils.runAndLogError;
2021

2122
import java.io.ByteArrayInputStream;
@@ -37,6 +38,7 @@
3738
import software.amazon.awssdk.core.exception.SdkClientException;
3839
import software.amazon.awssdk.core.http.HttpResponseHandler;
3940
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
41+
import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
4042
import software.amazon.awssdk.core.internal.util.ThrowableUtils;
4143
import software.amazon.awssdk.http.AbortableInputStream;
4244
import software.amazon.awssdk.http.SdkHttpFullResponse;
@@ -106,6 +108,18 @@ public class EventStreamAsyncResponseTransformer<ResponseT, EventT>
106108
*/
107109
private final AtomicReference<Throwable> error = new AtomicReference<>();
108110

111+
/**
112+
* The name of the aws service
113+
*/
114+
private final String serviceName;
115+
116+
/**
117+
* Request Id for the streaming request. The value is populated when the initial response is received from the service.
118+
* As request id is not sent in event messages (including exceptions), this can be returned by the SDK along with
119+
* received exception details.
120+
*/
121+
private String requestId = null;
122+
109123
/**
110124
* @param eventStreamResponseTransformer Response transformer provided by customer.
111125
* @param initialResponseUnmarshaller Unmarshaller for the initial-response event stream message.
@@ -115,19 +129,26 @@ public EventStreamAsyncResponseTransformer(
115129
EventStreamResponseHandler<ResponseT, EventT> eventStreamResponseTransformer,
116130
HttpResponseHandler<? extends ResponseT> initialResponseUnmarshaller,
117131
HttpResponseHandler<? extends EventT> eventUnmarshaller,
118-
HttpResponseHandler<? extends Throwable> exceptionUnmarshaller) {
132+
HttpResponseHandler<? extends Throwable> exceptionUnmarshaller,
133+
String serviceName) {
119134

120135
this.eventStreamResponseTransformer = eventStreamResponseTransformer;
121136
this.initialResponseUnmarshaller = initialResponseUnmarshaller;
122137
this.eventUnmarshaller = eventUnmarshaller;
123138
this.exceptionUnmarshaller = exceptionUnmarshaller;
139+
this.serviceName = serviceName;
124140
}
125141

126142
@Override
127143
public void responseReceived(SdkResponse response) {
128144
// We use a void unmarshaller and unmarshall the actual response in the message
129145
// decoder when we receive the initial-response frame. TODO not clear
130146
// how we would handle REST protocol which would unmarshall the response from the HTTP headers
147+
if (response != null && response.sdkHttpResponse() != null) {
148+
this.requestId = response.sdkHttpResponse()
149+
.firstMatchingHeader(X_AMZN_REQUEST_ID_HEADER)
150+
.orElse(null);
151+
}
131152
}
132153

133154
@Override
@@ -205,8 +226,17 @@ private MessageDecoder createDecoder() {
205226
EMPTY_EXECUTION_ATTRIBUTES));
206227
}
207228
} else if (isError(m) || isException(m)) {
208-
Throwable exception = exceptionUnmarshaller.handle(adaptMessageToResponse(m, true),
209-
EMPTY_EXECUTION_ATTRIBUTES);
229+
SdkHttpFullResponse errorResponse = adaptMessageToResponse(m, true);
230+
if (requestId != null) {
231+
errorResponse = errorResponse.toBuilder()
232+
.putHeader(X_AMZN_REQUEST_ID_HEADER, requestId)
233+
.build();
234+
}
235+
236+
Throwable exception = exceptionUnmarshaller.handle(errorResponse,
237+
new ExecutionAttributes()
238+
.putAttribute(SdkExecutionAttribute.SERVICE_NAME,
239+
serviceName));
210240
runAndLogError(log, "Error thrown from exceptionOccurred, ignoring.", () -> exceptionOccurred(exception));
211241
}
212242
} catch (Exception e) {

core/aws-core/src/main/java/software/amazon/awssdk/awscore/eventstream/EventStreamExceptionJsonUnmarshaller.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
import java.util.function.Supplier;
2323
import org.slf4j.Logger;
2424
import org.slf4j.LoggerFactory;
25+
import software.amazon.awssdk.annotations.ReviewBeforeRelease;
2526
import software.amazon.awssdk.annotations.SdkProtectedApi;
2627
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
2728
import software.amazon.awssdk.awscore.exception.AwsServiceException;
2829
import software.amazon.awssdk.core.SdkBytes;
2930
import software.amazon.awssdk.core.runtime.transform.JsonUnmarshallerContext;
3031
import software.amazon.awssdk.core.runtime.transform.Unmarshaller;
3132

33+
@ReviewBeforeRelease("Not currently used, keeping for backwards compatibility. Remove at GA.")
3234
@SdkProtectedApi
3335
public class EventStreamExceptionJsonUnmarshaller<T extends AwsServiceException>
3436
implements Unmarshaller<T, JsonUnmarshallerContext> {

core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/protocol/json/JsonErrorCodeParser.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
package software.amazon.awssdk.awscore.internal.protocol.json;
1717

1818
import com.fasterxml.jackson.databind.JsonNode;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.stream.Collectors;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
1925
import software.amazon.awssdk.annotations.SdkInternalApi;
2026
import software.amazon.awssdk.core.internal.protocol.json.JsonContent;
2127
import software.amazon.awssdk.http.SdkHttpFullResponse;
@@ -29,6 +35,17 @@ public class JsonErrorCodeParser implements ErrorCodeParser {
2935
*/
3036
public static final String X_AMZN_ERROR_TYPE = "x-amzn-ErrorType";
3137

38+
static final String ERROR_CODE_HEADER = ":error-code";
39+
40+
static final String EXCEPTION_TYPE_HEADER = ":exception-type";
41+
42+
private static final Logger log = LoggerFactory.getLogger(JsonErrorCodeParser.class);
43+
44+
/**
45+
* List of header keys that represent the error code sent by service.
46+
* Response should only contain one of these headers
47+
*/
48+
private final List<String> errorCodeHeaders;
3249
private final String errorCodeFieldName;
3350

3451
public JsonErrorCodeParser() {
@@ -37,6 +54,7 @@ public JsonErrorCodeParser() {
3754

3855
public JsonErrorCodeParser(String errorCodeFieldName) {
3956
this.errorCodeFieldName = errorCodeFieldName == null ? "__type" : errorCodeFieldName;
57+
this.errorCodeHeaders = Arrays.asList(X_AMZN_ERROR_TYPE, ERROR_CODE_HEADER, EXCEPTION_TYPE_HEADER);
4058
}
4159

4260
/**
@@ -60,7 +78,29 @@ public String parseErrorCode(SdkHttpFullResponse response, JsonContent jsonConte
6078
* present in the header.
6179
*/
6280
private String parseErrorCodeFromHeader(SdkHttpFullResponse response) {
63-
String headerValue = response.firstMatchingHeader(X_AMZN_ERROR_TYPE).orElse(null);
81+
Map<String, List<String>> filteredHeaders = response.headers().entrySet().stream()
82+
.filter(e -> errorCodeHeaders.contains(e.getKey()))
83+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
84+
85+
if (filteredHeaders.isEmpty()) {
86+
return null;
87+
}
88+
89+
if (filteredHeaders.size() > 1) {
90+
log.warn("Response contains multiple headers representing the error code: " + filteredHeaders.keySet());
91+
}
92+
93+
String headerKey = filteredHeaders.keySet().stream().findFirst().get();
94+
String headerValue = filteredHeaders.get(headerKey).get(0);
95+
96+
if (X_AMZN_ERROR_TYPE.equals(headerKey)) {
97+
return parseErrorCodeFromXAmzErrorType(headerValue);
98+
}
99+
100+
return headerValue;
101+
}
102+
103+
private String parseErrorCodeFromXAmzErrorType(String headerValue) {
64104
if (headerValue != null) {
65105
int separator = headerValue.indexOf(':');
66106
if (separator != -1) {

core/aws-core/src/main/java/software/amazon/awssdk/awscore/protocol/json/AwsJsonErrorMessageParser.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public final class AwsJsonErrorMessageParser implements ErrorMessageParser {
3333
*/
3434
private static final String X_AMZN_ERROR_MESSAGE = "x-amzn-error-message";
3535

36+
/**
37+
* Error message header returned by event stream errors
38+
*/
39+
private static final String EVENT_ERROR_MESSAGE = ":error-message";
40+
3641
private SdkJsonErrorMessageParser errorMessageParser;
3742

3843
/**
@@ -50,11 +55,16 @@ public AwsJsonErrorMessageParser(SdkJsonErrorMessageParser errorMessageJsonLocat
5055
*/
5156
@Override
5257
public String parseErrorMessage(SdkHttpFullResponse httpResponse, JsonNode jsonNode) {
53-
// If X_AMZN_ERROR_MESSAGE is present, prefer that. Otherwise check the JSON body.
5458
final String headerMessage = httpResponse.firstMatchingHeader(X_AMZN_ERROR_MESSAGE).orElse(null);
5559
if (headerMessage != null) {
5660
return headerMessage;
5761
}
62+
63+
final String eventHeaderMessage = httpResponse.firstMatchingHeader(EVENT_ERROR_MESSAGE).orElse(null);
64+
if (eventHeaderMessage != null) {
65+
return eventHeaderMessage;
66+
}
67+
5868
return errorMessageParser.parseErrorMessage(httpResponse, jsonNode);
5969
}
6070

core/aws-core/src/test/java/software/amazon/awssdk/awscore/eventstream/EventStreamAsyncResponseTransformerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private void verifyExceptionThrown(Map<String, HeaderValue> headers) {
5757

5858
AsyncResponseTransformer<SdkResponse, Void> transformer =
5959
new EventStreamAsyncResponseTransformer<>(new SubscribingResponseHandler(), null, null,
60-
(response, executionAttributes) -> exception);
60+
(response, executionAttributes) -> exception, null);
6161
transformer.responseReceived(null);
6262
transformer.onStream(SdkPublisher.adapt(bytePublisher));
6363

0 commit comments

Comments
 (0)