Skip to content

Commit

Permalink
Rename ApiException and ModeledApiException and move
Browse files Browse the repository at this point in the history
ApiException is renamed to CallException to better indicate that the
error may not have actually hit a service or be related to the API
(e.g., it could be a timeout, networking error, protocol error, etc.)

ModeledApiException is renamed to ModeledException since it no longer
extends from ApiException and Api isn't helping make the name more
clear.

Exceptions have been moved to a top-level error package, since not all
errors are modeled or have a schema, and to better group them.
  • Loading branch information
mtdowling committed Jan 28, 2025
1 parent 0ba5c97 commit 33239ef
Show file tree
Hide file tree
Showing 35 changed files with 384 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import java.util.stream.Collectors;
import software.amazon.eventstream.HeaderValue;
import software.amazon.eventstream.Message;
import software.amazon.smithy.java.core.schema.ModeledApiException;
import software.amazon.smithy.java.core.error.ModeledException;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.core.schema.SerializableStruct;
import software.amazon.smithy.java.core.schema.TraitKey;
Expand Down Expand Up @@ -79,7 +79,7 @@ public void writeStruct(Schema schema, SerializableStruct struct) {
public AwsEventFrame encodeFailure(Throwable exception) {
AwsEventFrame frame;
Schema exceptionSchema;
if (exception instanceof ModeledApiException me && (exceptionSchema = possibleExceptions.get(
if (exception instanceof ModeledException me && (exceptionSchema = possibleExceptions.get(
me.schema().id())) != null) {
var headers = new HashMap<String, HeaderValue>();
headers.put(":message-type", HeaderValue.fromString("exception"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import software.amazon.smithy.java.client.core.interceptors.RequestHook;
import software.amazon.smithy.java.client.core.interceptors.ResponseHook;
import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ApiException;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.SerializableStruct;
import software.amazon.smithy.java.logging.InternalLogger;
Expand Down Expand Up @@ -260,7 +260,7 @@ private <I extends SerializableStruct, O extends SerializableStruct> ResolvedSch
for (var authSchemeOption : authSchemeOptions) {
options.add(authSchemeOption.schemeId().toString());
}
throw new ApiException(
throw new CallException(
"No auth scheme could be resolved for operation " + call.operation.schema().id()
+ "; protocol=" + protocol.id()
+ "; requestClass=" + request.getClass()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.util.concurrent.CompletableFuture;
import software.amazon.smithy.java.client.core.endpoint.Endpoint;
import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ApiException;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.SerializableStruct;
import software.amazon.smithy.java.core.serde.TypeRegistry;
Expand Down Expand Up @@ -79,7 +79,7 @@ <I extends SerializableStruct, O extends SerializableStruct> RequestT createRequ
* @param request Request that was sent for this response.
* @param response Response to deserialize.
* @return the deserialized output shape.
* @throws ApiException if an error occurs, including deserialized modeled errors and protocol errors.
* @throws CallException if an error occurs, including deserialized modeled errors and protocol errors.
*/
<I extends SerializableStruct, O extends SerializableStruct> CompletableFuture<O> deserializeResponse(
ApiOperation<I, O> operation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import software.amazon.smithy.java.client.core.ClientPlugin;
import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor;
import software.amazon.smithy.java.client.core.interceptors.OutputHook;
import software.amazon.smithy.java.core.schema.ApiException;
import software.amazon.smithy.java.core.schema.ModeledApiException;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.error.ModeledException;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.core.schema.SerializableStruct;
import software.amazon.smithy.java.core.schema.TraitKey;
Expand Down Expand Up @@ -40,14 +40,14 @@ public <O extends SerializableStruct> O modifyBeforeAttemptCompletion(
OutputHook<?, O, ?, ?> hook,
RuntimeException error
) {
if (error instanceof ApiException ae && ae.isRetrySafe() == RetrySafety.MAYBE) {
applyRetryInfoFromModel(hook.operation().schema(), ae);
if (error instanceof CallException ce && ce.isRetrySafe() == RetrySafety.MAYBE) {
applyRetryInfoFromModel(hook.operation().schema(), ce);
}
return hook.forward(error);
}
}

static void applyRetryInfoFromModel(Schema operationSchema, ApiException e) {
static void applyRetryInfoFromModel(Schema operationSchema, CallException e) {
// If the operation is readonly or idempotent, then it's safe to retry (other checks can disqualify later).
var isRetryable = operationSchema.hasTrait(TraitKey.READ_ONLY_TRAIT)
|| operationSchema.hasTrait(TraitKey.IDEMPOTENT_TRAIT);
Expand All @@ -57,7 +57,7 @@ static void applyRetryInfoFromModel(Schema operationSchema, ApiException e) {
}

// If the exception is modeled as retryable or a throttle, then use that information.
if (e instanceof ModeledApiException mae) {
if (e instanceof ModeledException mae) {
var retryTrait = mae.schema().getTrait(TraitKey.RETRYABLE_TRAIT);
if (retryTrait != null) {
e.isRetrySafe(RetrySafety.YES);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.core.plugins;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import software.amazon.smithy.java.client.core.ClientConfig;
import software.amazon.smithy.java.client.core.ClientPlugin;
import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor;
import software.amazon.smithy.java.client.core.interceptors.OutputHook;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.schema.SerializableStruct;

/**
* Converts exceptions so that they adhere to client-specific requirements.
*
* <p>This plugin allows for:
* <ul>
* <li>Converting specific exceptions by class equality to another exception using
* {@link Builder#convert(Class, Function)}.</li>
* <li>Throwing exceptions as-is if they extend from a specific exception using
* {@link Builder#baseApiExceptionMapper(Class, Function)}.</li>
* <li>Changing {@link CallException}s that don't extend from a specific ApiException into the desired
* ApiException subtype using {@link Builder#baseApiExceptionMapper(Class, Function)}.</li>
* <li>Converting all other totally unknown exceptions into another kind of exception type using
* {@link Builder#rootExceptionMapper}</li>
* </ul>
*
* <pre>{@code
* ExceptionMapper mapper = ExceptionMapper.builder()
* .convert(SomeSpecificError.class, e -> new OtherError(e.getMessage(), e))
* .baseApiExceptionMapper(MyError.class, e -> {
* return switch(e.getFault()) {
* case CLIENT -> new MyClientError(e);
* case SERVER -> new MyServerError(e);
* default -> new MyError(e);
* }
* })
* .rootExceptionMapper(e -> new MyClientError(e))
* .build();
* }</pre>
*/
public final class ExceptionMapper implements ClientPlugin {

private final Map<Class<? extends RuntimeException>,
Function<RuntimeException, RuntimeException>> mappers = new HashMap<>();
private final Class<? extends CallException> baseApiExceptionType;
private final Function<CallException, ? extends CallException> baseApiExceptionMapper;
private final Function<RuntimeException, ? extends RuntimeException> rootExceptionMapper;

private ExceptionMapper(Builder builder) {
this.mappers.putAll(builder.mappers);
this.baseApiExceptionType = builder.baseApiExceptionType;
this.baseApiExceptionMapper = builder.baseApiExceptionMapper;
this.rootExceptionMapper = builder.rootExceptionMapper;
}

@Override
public void configureClient(ClientConfig.Builder config) {
config.addInterceptor(new ExceptionInterceptor());
}

private final class ExceptionInterceptor implements ClientInterceptor {
@Override
public <O extends SerializableStruct> O modifyBeforeCompletion(
OutputHook<?, O, ?, ?> hook,
RuntimeException error
) {
if (error == null) {
return hook.output();
}

// Perform specific error conversions.
var mapper = mappers.get(error.getClass());
if (mapper != null) {
throw mapper.apply(error);
}

// Perform base API error conversions for unknown ApiExceptions.
if (baseApiExceptionType != null && error instanceof CallException a) {
throw baseApiExceptionType.isInstance(error) ? error : baseApiExceptionMapper.apply(a);
}

throw rootExceptionMapper.apply(error);
}
}

/**
* Builds up an exception mapper.
*/
public static final class Builder {
private final Map<Class<? extends RuntimeException>,
Function<RuntimeException, RuntimeException>> mappers = new HashMap<>();
private Class<? extends CallException> baseApiExceptionType;
private Function<CallException, ? extends CallException> baseApiExceptionMapper;
private Function<RuntimeException, ? extends RuntimeException> rootExceptionMapper = Function.identity();

public ExceptionMapper build() {
return new ExceptionMapper(this);
}

/**
* Converts a specific error to another kind of error.
*
* <p>These explicit conversions are check first before applying {@link #baseApiExceptionMapper} or
* {@link #rootExceptionMapper}.
*
* @param exception Exception to convert.
* @param mapper The mapper used to create the converted exception.
* @return the builder.
* @param <C> The exception to convert.
*/
@SuppressWarnings("unchecked")
public <C extends RuntimeException> Builder convert(Class<C> exception, Function<C, RuntimeException> mapper) {
mappers.put(exception, (Function<RuntimeException, RuntimeException>) mapper);
return this;
}

/**
* Converts instances of {@link CallException} that don't implement the given {@code baseType} into an exception
* that does extend from it using the given {@code mapper} function.
*
* <p>Setting a baseApiExceptionMapper is optional. This allows clients to create a base exception type for
* all API-level exceptions.
*
* @param baseType The base type ApiExceptions should extend from.
* @param mapper A mapper used to convert the found exception into a subtype of {@code baseType}.
* @return the builder.
* @param <C> the base type.
*/
public <C extends CallException> Builder baseApiExceptionMapper(
Class<C> baseType,
Function<CallException, ? extends C> mapper
) {
this.baseApiExceptionType = baseType;
this.baseApiExceptionMapper = mapper;
return this;
}

/**
* Overrides the root exception mapper, used to convert completely unknown errors that don't extend from
* {@link CallException}.
*
* <p>By default, the root exception mapper simply throws the given exception as-is.
*
* @param mapper The root-level exception mapper used to convert the given error or return it as-is.
* @return the builder.
*/
public Builder rootExceptionMapper(Function<RuntimeException, ? extends RuntimeException> mapper) {
this.rootExceptionMapper = Objects.requireNonNull(mapper, "rootExceptionMapper cannot be null");
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;
import software.amazon.smithy.java.core.schema.ApiException;
import software.amazon.smithy.java.core.schema.ModeledApiException;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.error.ModeledException;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.core.serde.ShapeSerializer;
import software.amazon.smithy.java.retries.api.RetrySafety;
Expand All @@ -23,7 +23,7 @@ public class ApplyModelRetryInfoPluginTest {
@Test
public void marksSafeWhenOperationIsReadOnly() {
var schema = Schema.createOperation(ShapeId.from("com#Foo"), new ReadonlyTrait());
var e = new ApiException("err");
var e = new CallException("err");

ApplyModelRetryInfoPlugin.applyRetryInfoFromModel(schema, e);

Expand All @@ -33,7 +33,7 @@ public void marksSafeWhenOperationIsReadOnly() {
@Test
public void marksSafeWhenOperationIsIdempotent() {
var schema = Schema.createOperation(ShapeId.from("com#Foo"), new IdempotentTrait());
var e = new ApiException("err");
var e = new CallException("err");

ApplyModelRetryInfoPlugin.applyRetryInfoFromModel(schema, e);

Expand All @@ -50,7 +50,7 @@ public void marksSafeWhenErrorIsRetryable() {
.structureBuilder(ShapeId.from("com#Err"), RetryableTrait.builder().throttling(true).build())
.build();

var e = new ModeledApiException(errorSchema, "err") {
var e = new ModeledException(errorSchema, "err") {
@Override
public void serializeMembers(ShapeSerializer serializer) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import software.amazon.smithy.java.client.http.HttpClientProtocol;
import software.amazon.smithy.java.client.http.HttpErrorDeserializer;
import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ApiException;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.schema.ApiOperation;
import software.amazon.smithy.java.core.schema.InputEventStreamingApiOperation;
import software.amazon.smithy.java.core.schema.OutputEventStreamingApiOperation;
Expand Down Expand Up @@ -145,7 +145,7 @@ protected boolean isSuccess(ApiOperation<?, ?> operation, Context context, HttpR
* @return Returns the deserialized error.
*/
protected <I extends SerializableStruct,
O extends SerializableStruct> CompletableFuture<? extends ApiException> createError(
O extends SerializableStruct> CompletableFuture<? extends CallException> createError(
ApiOperation<I, O> operation,
Context context,
TypeRegistry typeRegistry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import java.util.concurrent.CompletableFuture;
import software.amazon.smithy.java.client.http.HttpErrorDeserializer;
import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ModeledApiException;
import software.amazon.smithy.java.core.error.ModeledException;
import software.amazon.smithy.java.core.schema.ShapeBuilder;
import software.amazon.smithy.java.core.serde.Codec;
import software.amazon.smithy.java.http.api.HttpResponse;
Expand All @@ -33,11 +33,11 @@ public HttpBindingErrorFactory(HttpBinding httpBinding) {
}

@Override
public CompletableFuture<ModeledApiException> createError(
public CompletableFuture<ModeledException> createError(
Context context,
Codec codec,
HttpResponse response,
ShapeBuilder<ModeledApiException> builder
ShapeBuilder<ModeledException> builder
) {
return httpBinding.responseDeserializer()
.payloadCodec(codec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import software.amazon.smithy.java.client.http.AmznErrorHeaderExtractor;
import software.amazon.smithy.java.client.http.HttpErrorDeserializer;
import software.amazon.smithy.java.context.Context;
import software.amazon.smithy.java.core.schema.ApiException;
import software.amazon.smithy.java.core.schema.ModeledApiException;
import software.amazon.smithy.java.core.error.CallException;
import software.amazon.smithy.java.core.error.ModeledException;
import software.amazon.smithy.java.core.schema.Schema;
import software.amazon.smithy.java.core.schema.ShapeBuilder;
import software.amazon.smithy.java.core.serde.Codec;
Expand Down Expand Up @@ -63,7 +63,8 @@ public void usesGenericErrorWhenPayloadTypeIsUnknown() throws Exception {
.serviceId(SERVICE)
.knownErrorFactory(new HttpBindingErrorFactory())
.unknownErrorFactory(
(fault, message, response) -> CompletableFuture.completedFuture(new ApiException("Hi!", fault)))
(fault, message, response) -> CompletableFuture
.completedFuture(new CallException("Hi!", fault)))
.build();
var registry = TypeRegistry.builder()
.putType(Baz.SCHEMA.id(), Baz.class, Baz.Builder::new)
Expand All @@ -74,7 +75,7 @@ public void usesGenericErrorWhenPayloadTypeIsUnknown() throws Exception {
var response = responseBuilder.build();
var result = deserializer.createError(Context.create(), OPERATION, registry, response).get();

assertThat(result, instanceOf(ApiException.class));
assertThat(result, instanceOf(CallException.class));
assertThat(result.getMessage(), equalTo("Hi!"));
}

Expand All @@ -86,7 +87,8 @@ public void usesGenericErrorWhenHeaderTypeIsUnknown() throws Exception {
.knownErrorFactory(new HttpBindingErrorFactory())
.headerErrorExtractor(new AmznErrorHeaderExtractor())
.unknownErrorFactory(
(fault, message, response) -> CompletableFuture.completedFuture(new ApiException("Hi!", fault)))
(fault, message, response) -> CompletableFuture
.completedFuture(new CallException("Hi!", fault)))
.build();
var registry = TypeRegistry.builder()
.putType(Baz.SCHEMA.id(), Baz.class, Baz.Builder::new)
Expand All @@ -104,11 +106,11 @@ public void usesGenericErrorWhenHeaderTypeIsUnknown() throws Exception {
var response = responseBuilder.build();
var result = deserializer.createError(Context.create(), OPERATION, registry, response).get();

assertThat(result, instanceOf(ApiException.class));
assertThat(result, instanceOf(CallException.class));
assertThat(result.getMessage(), equalTo("Hi!"));
}

static final class Baz extends ModeledApiException {
static final class Baz extends ModeledException {

static final Schema SCHEMA = Schema
.structureBuilder(ShapeId.from("com.foo#Baz"), new ErrorTrait("client"))
Expand Down
Loading

0 comments on commit 33239ef

Please sign in to comment.