CLOVER🍀

That was when it all began.

AWS SDK for Java 2.xでAPI呼び出しの結果がエラーの場合、例外がスローされるのか?(SdkHttpResponseのisSuccessfulは?)

これは、なにをしたくて書いたもの?

AWS SDK for Java 2.xを使うと、APIの各種レスポンスからSdkHttpResponseというインターフェースのインスタンスを取得することが
できます。

こちらですね。

SdkHttpResponse (AWS SDK for Java - 2.20.17)

このクラスにはisSuccessfulという成功/失敗を判定できそうなメソッドがあります。

If we get back any 2xx status code, then we know we should treat the service call as successful.

APIの呼び出し結果を扱う時に、エラーハンドリングとしてこの値を見た方がいいのかどうなのかよくわからなかったので、ちょっと
調べてみることにしました。

Amazon S3を例に

Amazon S3を例に、ちょっと見てみましょう。

Amazon S3にアクセスする際には、S3Clientというインターフェース(またはS3AsyncClient)を使います。

S3Client (AWS SDK for Java - 2.20.17)

バケットを作成するcreateBucketを見てみると、問題があった時には以下のように例外がスローされるであろうことがわかります。

default CreateBucketResponse createBucket(CreateBucketRequest createBucketRequest)
                                   throws BucketAlreadyExistsException,
                                          BucketAlreadyOwnedByYouException,
                                          AwsServiceException,
                                          SdkClientException,
                                          S3Exception

一方で、その戻り値であるCreateBucketResponseの定義を見ると、sdkHttpResponseというメソッドがあり。

CreateBucketResponse (AWS SDK for Java - 2.20.17)

こちらから先述のSdkHttpResponseを得ることができ、isSuccessfulというメソッドを備えています。

これを見ていると、スローされる例外とは別にSdkHttpResponse#isSuccessfulの結果も見ないといけないのでは?と思ってしまったのが
今回のエントリーを書いた理由ですね。

ドキュメントはどう言っているか?

ここで、AWS SDK for Java 2.xの例外処理のドキュメントを見てみましょう。

AWS SDK for Java 2.x の例外処理 - AWS SDK for Java 2.x

AwsServiceExceptionおよびそのサブクラスに関する記述を見ると、API呼び出し時にエラーが発生した時はAwsServiceExceptionの
インスタンス(およびそのサブクラス含む)がスローされることが書かれています。

AWS SDK for Java 2.x の例外処理 / AwsServiceException (およびサブクラス)

たとえば、最初に書いたS3Client#createBucketメソッドからスローされるとされているBucketAlreadyOwnedByYouExceptionクラスも、
AwsServiceExceptionのサブクラスですね。

BucketAlreadyOwnedByYouException (AWS SDK for Java - 2.20.17)

例外の詳細については、AwsErrorDetailsクラスを確認すればよい、となっています。

AwsErrorDetails (AWS SDK for Java - 2.20.17)

こちらのerrorCodeから、各サービスのドキュメントを確認することができます。たとえば、Amazon S3の場合はこちら。

Error Responses / List of Error Codes

AwsErrorDetailsのインスタンスは、AwsServiceExceptionから取得できます。

AwsServiceException (AWS SDK for Java - 2.20.17)

また、そもそもAPI呼び出しができないような状況の時にはSdkClientExceptionがスローされる、とも書かれています。

AWS SDK for Java 2.x の例外処理 / SdkClientException

こちらですね。

SdkClientException (AWS SDK for Java - 2.20.17)

どちらにせよ、SdkHttpResponseインターフェースはまったく登場しません。

このことから、APIの呼び出しに失敗すると例外がスローされるので、API呼び出しの成功確認にSdkHttpResponse#isSuccessfulメソッドを
参照しなくても良さそうな気がしますね。

今回、こちらをもう少し追ってみましょう。LocalStackとAWS SDK for Java 2.xのS3Clientを使って試してみたいと思います。

環境

今回の環境は、こちら。

LocalStack。

$ python3 -V
Python 3.10.6


$ localstack --version
1.4.0

起動。

$ localstack start

Javaまわり。

$ java --version
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment (build 17.0.6+10-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.6+10-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.0 (9b58d2bad23a66be161c4664ef21ce219c2c8584)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.6, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-67-generic", arch: "amd64", family: "unix"

準備

Maven依存関係などは、こちら。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>software.amazon.awssdk</groupId>
                <artifactId>bom</artifactId>
                <version>2.20.17</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
        </dependency>
    </dependencies>

サンプルプログラムを書く

今回用意したプログラムは、こちら。

src/main/java/org/littlewings/aws/App.java

package org.littlewings.aws;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.http.HttpStatusFamily;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.s3.model.CreateBucketResponse;

import java.net.URI;

public class App {
    public static void main(String... args) {
        try (S3Client s3Client = S3Client
                                    .builder()
                                    .credentialsProvider(
                                            StaticCredentialsProvider.create(AwsBasicCredentials.create("access_key_id", "secret_access_key"))
                                    )
                                    .region(Region.AP_EAST_1)
                                    .defaultsMode(DefaultsMode.AUTO)
                                    .endpointOverride(URI.create("http://localhost:4566"))
                                    .forcePathStyle(true)
                                    .build()) {

            CreateBucketRequest request =
                    CreateBucketRequest
                            .builder()
                            .bucket("my-bucket")
                            .build();

            CreateBucketResponse response = s3Client.createBucket(request);

            SdkHttpResponse httpResponse = response.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));
        } catch (AwsServiceException e) {
            AwsErrorDetails errorDetails = e.awsErrorDetails();
            System.out.printf("serviceName = %s%n", errorDetails.serviceName());
            System.out.printf("errorMessage = %s%n", errorDetails.errorMessage());
            System.out.printf("errorCode = %s%n", errorDetails.errorCode());

            SdkHttpResponse httpResponse = errorDetails.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));

            e.printStackTrace();
        }
    }
}

Amazon S3バケットを作成するだけのプログラムです。

            CreateBucketRequest request =
                    CreateBucketRequest
                            .builder()
                            .bucket("my-bucket")
                            .build();

            CreateBucketResponse response = s3Client.createBucket(request);

あとは、レスポンスに含まれるSdkHttpResponseの内容を見たり、

            SdkHttpResponse httpResponse = response.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));

例外ハンドリングを行ったり。

        } catch (AwsServiceException e) {
            AwsErrorDetails errorDetails = e.awsErrorDetails();
            System.out.printf("serviceName = %s%n", errorDetails.serviceName());
            System.out.printf("errorMessage = %s%n", errorDetails.errorMessage());
            System.out.printf("errorCode = %s%n", errorDetails.errorCode());

            SdkHttpResponse httpResponse = errorDetails.sdkHttpResponse();
            System.out.printf("isSuccessful = %b%n", httpResponse.isSuccessful());
            System.out.printf("statusCode = %d%n", httpResponse.statusCode());
            System.out.printf("httpStatusFamily = %s%n", HttpStatusFamily.of(httpResponse.statusCode()));

            e.printStackTrace();
        }

スローされる例外はAwsServiceExceptionクラスのサブクラスなので、こちらで一気に捕捉することにします。

確認してみる

作成したプログラムを動かしてみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.aws.App

結果。

isSuccessful = true
statusCode = 200
httpStatusFamily = SUCCESSFUL

もう1度実行すると、すでにS3バケットは作成されているので例外がスローされます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.aws.App

こんな感じですね。

serviceName = S3
errorMessage = Your previous request to create the named bucket succeeded and you already own it.
errorCode = BucketAlreadyOwnedByYou
isSuccessful = false
statusCode = 409
httpStatusFamily = CLIENT_ERROR
software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException: Your previous request to create the named bucket succeeded and you already own it. (Service: S3, Status Code: 409, Request ID: 44425877V1D0A2F9, Extended Request ID: MzRISOwyjmnup2476E87BD8C5CA707/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleErrorResponse(AwsXmlPredicatedResponseHandler.java:156)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleResponse(AwsXmlPredicatedResponseHandler.java:108)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:85)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:43)
        at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler$Crc32ValidationResponseHandler.handle(AwsSyncClientHandler.java:95)
        at software.amazon.awssdk.core.internal.handler.BaseClientHandler.lambda$successTransformationResponseHandler$7(BaseClientHandler.java:270)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:40)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.HandleResponseStage.execute(HandleResponseStage.java:30)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:73)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:78)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:40)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:50)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptMetricCollectionStage.execute(ApiCallAttemptMetricCollectionStage.java:36)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:81)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:36)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:56)
        at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:36)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:80)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:60)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:42)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:48)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallMetricCollectionStage.execute(ApiCallMetricCollectionStage.java:31)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37)
        at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26)
        at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:193)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:103)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.doExecute(BaseSyncClientHandler.java:171)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.lambda$execute$1(BaseSyncClientHandler.java:82)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.measureApiCallSuccess(BaseSyncClientHandler.java:179)
        at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:76)
        at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:45)
        at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:56)
        at software.amazon.awssdk.services.s3.DefaultS3Client.createBucket(DefaultS3Client.java:1149)
        at org.littlewings.aws.App.main(App.java:36)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)

今回はBucketAlreadyOwnedByYouExceptionがスローされました。

software.amazon.awssdk.services.s3.model.BucketAlreadyOwnedByYouException: Your previous request to create the named bucket succeeded and you already own it. (Service: S3, Status Code: 409, Request ID: 44425877V1D0A2F9, Extended Request ID: MzRISOwyjmnup2476E87BD8C5CA707/JypPGXLh0OVFGcJaaO3KW/hRAqKOpIEEp)

例外に含まれているAwsErrorDetailsから得られた情報はこちら。

serviceName = S3
errorMessage = Your previous request to create the named bucket succeeded and you already own it.
errorCode = BucketAlreadyOwnedByYou

AwsErrorDetailsから取得できる、SdkHttpResponseの情報はこちら。

isSuccessful = false
statusCode = 409
httpStatusFamily = CLIENT_ERROR

このケースではSdkHttpResponse#isSuccessfulがfalseになっています。そもそも、HTTPステータスコードが2xxの場合にtrueになる
プロパティですからね。

If we get back any 2xx status code, then we know we should treat the service call as successful.

というわけで、SdkHttpResponse#isSuccessfulがfalseになるケースではやはり例外がスローされる気がします。

実装は?

少し、実装を見てみましょう。S3Clientインターフェースの実装クラスを少し見てみます。

$ unzip -p $HOME/.m2/repository/software/amazon/awssdk/s3/2.20.17/s3-2.20.17-sources.jar software/amazon/awssdk/services/s3/DefaultS3Client.java

こんな感じで、エラーコードと例外がマッピングされている箇所がありました。

    private AwsS3ProtocolFactory init() {
        return AwsS3ProtocolFactory
                .builder()
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchUpload")
                                .exceptionBuilderSupplier(NoSuchUploadException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectAlreadyInActiveTierError")
                                .exceptionBuilderSupplier(ObjectAlreadyInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyExists")
                                .exceptionBuilderSupplier(BucketAlreadyExistsException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchBucket")
                                .exceptionBuilderSupplier(NoSuchBucketException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("InvalidObjectState")
                                .exceptionBuilderSupplier(InvalidObjectStateException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectNotInActiveTierError")
                                .exceptionBuilderSupplier(ObjectNotInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyOwnedByYou")
                                .exceptionBuilderSupplier(BucketAlreadyOwnedByYouException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchKey").exceptionBuilderSupplier(NoSuchKeyException::builder)
                                .build()).clientConfiguration(clientConfiguration)
                .defaultServiceExceptionSupplier(S3Exception::builder).build();
    }

先ほどのサンプルアプリケーションでS3バケットの作成失敗時に、AwsErrorDetailsから得られたエラーコードはBucketAlreadyOwnedByYou
なので、こちらが該当しますね。

                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyOwnedByYou")
                                .exceptionBuilderSupplier(BucketAlreadyOwnedByYouException::builder).build())

マッピングされるエラーコードがない場合は、S3Exceptionがスローされるように見えます。

                .defaultServiceExceptionSupplier(S3Exception::builder).build();

こう見ると、S3Client#createBucketのthrowsに書かれている内容がわかってきますね。

default CreateBucketResponse createBucket(CreateBucketRequest createBucketRequest)
                                   throws BucketAlreadyExistsException,
                                          BucketAlreadyOwnedByYouException,
                                          AwsServiceException,
                                          SdkClientException,
                                          S3Exception

エラーコードがマッピングできた場合はBucketAlreadyExistsException、BucketAlreadyOwnedByYouException、それ以外は
S3Exception、AwsServiceException、そもそもAPI呼び出しができなかった場合はSdkClientExceptionという感じですね。

S3AsyncClientの実装も見てみましたが、同じ感じでした。

$ unzip -p $HOME/.m2/repository/software/amazon/awssdk/s3/2.20.17/s3-2.20.17-sources.jar software/amazon/awssdk/services/s3/DefaultS3AsyncClient.java

こちらですね。

    private AwsS3ProtocolFactory init() {
        return AwsS3ProtocolFactory
                .builder()
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchUpload")
                                .exceptionBuilderSupplier(NoSuchUploadException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectAlreadyInActiveTierError")
                                .exceptionBuilderSupplier(ObjectAlreadyInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyExists")
                                .exceptionBuilderSupplier(BucketAlreadyExistsException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchBucket")
                                .exceptionBuilderSupplier(NoSuchBucketException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("InvalidObjectState")
                                .exceptionBuilderSupplier(InvalidObjectStateException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("ObjectNotInActiveTierError")
                                .exceptionBuilderSupplier(ObjectNotInActiveTierErrorException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("BucketAlreadyOwnedByYou")
                                .exceptionBuilderSupplier(BucketAlreadyOwnedByYouException::builder).build())
                .registerModeledException(
                        ExceptionMetadata.builder().errorCode("NoSuchKey").exceptionBuilderSupplier(NoSuchKeyException::builder)
                                .build()).clientConfiguration(clientConfiguration)
                .defaultServiceExceptionSupplier(S3Exception::builder).build();
    }

ところで、AWS SDK for Java 2.xのGitHubリポジトリを見ても、これらのクラスは存在しません。

GitHub - aws/aws-sdk-java-v2: The official AWS SDK for Java - Version 2

リクエスト、レスポンスまわりのクラスの雰囲気からもなんとなく察しがつきますが、これらのクラスは自動生成な感じがしますね。

@Generated("software.amazon.awssdk:codegen")
@SdkInternalApi
final class DefaultS3Client implements S3Client {


...


@Generated("software.amazon.awssdk:codegen")
public final class CreateBucketResponse extends S3Response implements


...


@Generated("software.amazon.awssdk:codegen")
public final class BucketAlreadyOwnedByYouException extends S3Exception implements
        ToCopyableBuilder<BucketAlreadyOwnedByYouException.Builder, BucketAlreadyOwnedByYouException> {

ここでは、このあたりは追いませんが。

気にするのは、先ほどのスタックトレースで出現していたこのあたりかなと。

        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleErrorResponse(AwsXmlPredicatedResponseHandler.java:156)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handleResponse(AwsXmlPredicatedResponseHandler.java:108)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:85)
        at software.amazon.awssdk.protocols.xml.internal.unmarshall.AwsXmlPredicatedResponseHandler.handle(AwsXmlPredicatedResponseHandler.java:43)
        at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler$Crc32ValidationResponseHandler.handle(AwsSyncClientHandler.java:95)
        at software.amazon.awssdk.core.internal.handler.BaseClientHandler.lambda$successTransformationResponseHandler$7(BaseClientHandler.java:270

BaseClientHandlerでレスポンスのハンドリングを行っている箇所は、こちら。API呼び出しが成功した場合は、Response内に
SdkHttpResponseなどが含まれることになります。

            Response<OutputT> delegateResponse = responseHandler.handle(response, executionAttributes);

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java#L270

そして、APIの呼び出し結果が成功/失敗による分岐。

        if (parsedResponse.isResponseSuccess()) {
            OutputT response = handleSuccessResponse(parsedResponse);
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .response(response)
                                              .isSuccess(true)
                                              .build();
        } else {
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .exception(handleErrorResponse(parsedResponse))
                                              .isSuccess(false)
                                              .build();
        }

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/AwsXmlPredicatedResponseHandler.java#L100-L111

handleErrorResponseメソッドでは、実際にスローされる例外が作成されます。SdkExceptionの部分ですね。

            SdkException exception = errorResponseTransformer.apply(parsedResponse);
            exception.fillInStackTrace();
            return exception;

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/AwsXmlPredicatedResponseHandler.java#L155-L157

エラーコードから、例外に変換しているのは以下の箇所のようです。一致するものがなかった場合は、デフォルトの例外が選択される
ようです。

        AwsServiceException.Builder builder = errorRoot
            .map(e -> invokeSafely(() -> unmarshallFromErrorCode(response, e, errorCode)))
            .orElseGet(this::defaultException);

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-query-protocol/src/main/java/software/amazon/awssdk/protocols/query/internal/unmarshall/AwsXmlErrorUnmarshaller.java#L80-L82

そして、この例外がスローされることになる、というわけですね。

あとは、先ほどの以下の分岐でtrue/falseになる情報を見ると良さそうです。

        if (parsedResponse.isResponseSuccess()) {
            OutputT response = handleSuccessResponse(parsedResponse);
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .response(response)
                                              .isSuccess(true)
                                              .build();
        } else {
            return Response.<OutputT>builder().httpResponse(httpResponse)
                                              .exception(handleErrorResponse(parsedResponse))
                                              .isSuccess(false)
                                              .build();
        }

これは、以下の部分ですね。

        if (!context.sdkHttpFullResponse().isSuccessful()) {
            // Request was non-2xx, defer to protocol handler for error root
            Optional<XmlElement> parsedErrorXml = parsedRootXml.flatMap(errorRootLocationFunction);
            return context.toBuilder().isResponseSuccess(false).parsedErrorXml(parsedErrorXml.orElse(null)).build();
        }

https://github.com/aws/aws-sdk-java-v2/blob/2.20.17/core/protocols/aws-xml-protocol/src/main/java/software/amazon/awssdk/protocols/xml/internal/unmarshall/DecorateErrorFromResponseBodyUnmarshaller.java#L58-L62

ここでSdkHttpFullReponseというのは、SdkHttpResponseインターフェースを拡張したものです。

SdkHttpFullResponse (AWS SDK for Java - 2.20.17)

ということは、SdkHttpFullReponse#isSuccessfulがfalseの時に例外がスローされることになるので、正常にAPI呼び出しが完了した場合は
SdkHttpResponse#isSuccessfulはtrueになり、falseになるケースでは例外がスローされることになります。

なので、AWS SDK for Java 2.xの例外処理ではSdkHttpResponse#isSuccessfulの話は出てこない、となりそうです。

AWS SDK for Java 2.x の例外処理 - AWS SDK for Java 2.x

実行パスを全部見たかというとそういうわけでもないのですが、考え方としてはそんなに誤っていないと思うのですが…どうでしょう?

なお、API呼び出しが正常にできた場合にレスポンスのオブジェクトを見ないでいいかというと、レスポンスから得たい情報はAPI次第かと
思います(呼び出し結果の値に関心があるかどうか)。

まとめ

AWS SDK for Java 2.xのSdkHttpResponse#isSuccessfulが気になったので、ちょっと確認してみました。

例外がスローされるかどうかに加えて、SdkHttpResponse#isSuccessfulを見る必要があるかどうかで迷っていたのですが、これはどうやら
スルーして良さそうです。

レスポンスのオブジェクトからは、API呼び出しが正常に終了した場合に欲しい情報を取得するのに使う感じですね。