AWS SDK for Rust の複数サービスのエラーをまとめる

公式の aws-sdk-rust を使うときの話です。まだ pre-release なので今後のバージョンでは話が変わるかもです。現在の最新である 0.10.1 での話です。

初期状態

もともとこんな感じでアプリケーションのエラーをまとめた enum を用意していました。これなら、main でやっているように ? が使えます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("some application error has been occurred.")]
AppHoge,

#[error("aws api error has been occurred")]
Aws(#[from] aws_sdk_ec2::Error),
}

impl<T> From<aws_sdk_ec2::types::SdkError<T>> for MyError
where
aws_sdk_ec2::types::SdkError<T>: Into<aws_sdk_ec2::Error>,
{
fn from(e: aws_sdk_ec2::types::SdkError<T>) -> Self {
MyError::Aws(e.into())
}
}

#[tokio::main]
async fn main() -> Result<(), MyError> {
let config = aws_config::from_env().load().await;
let ec2 = aws_sdk_ec2::Client::new(&config);

let ec2_result = ec2.describe_instances().send().await?;

// do something

Ok(())
}

問題発覚

EC2 以外の別サービスの呼び出しを追加した状態です。ここでは S3 の呼び出しを追加してあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
#[tokio::main]
async fn main() -> Result<(), MyError> {
let config = aws_config::from_env().load().await;
let ec2 = aws_sdk_ec2::Client::new(&config);
let s3 = aws_sdk_s3::Client::new(&config);

let ec2_result = ec2.describe_instances().send().await?;
let s3_result = s3.list_buckets().send().await?;

// do something

Ok(())
}

これはコンパイルできなくて以下のエラーになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error[E0277]: `?` couldn't convert the error to `aws_sdk_ec2::Error`
--> src/main.rs:26:51
|
26 | let s3_result = s3.list_buckets().send().await?;
| ^ the trait `From<SdkError<ListBucketsError>>` is not implemented for `aws_sdk_ec2::Error`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= help: the following implementations were found:
<aws_sdk_ec2::Error as From<SdkError<AcceptReservedInstancesExchangeQuoteError, R>>>
<aws_sdk_ec2::Error as From<SdkError<AcceptTransitGatewayMulticastDomainAssociationsError, R>>>
<aws_sdk_ec2::Error as From<SdkError<AcceptTransitGatewayPeeringAttachmentError, R>>>
<aws_sdk_ec2::Error as From<SdkError<AcceptTransitGatewayVpcAttachmentError, R>>>
and 518 others
= note: required because of the requirements on the impl of `Into<aws_sdk_ec2::Error>` for `SdkError<ListBucketsError>`
= note: required because of the requirements on the impl of `From<SdkError<ListBucketsError>>` for `MyError`
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, SdkError<ListBucketsError>>>` for `Result<(), MyError>`

aws_sdk_s3::types::SdkError<T>aws_sdk_ec2::Error に変換できません。ここでまず思うのは「サービスをまたいだ共通のエラー構造体はあるのかな?」ということです。実は aws_sdk_s3::types::SdkError<T>aws_smithy_http::result::SdkError<T> です。aws_sdk_ec2::types::SdkError<T> も同様です。

そのため以下のようにエラーを表現することができるはできます。ただ、これをやると MyError を利用する関数が全部 generic になって厳しいです。うまいことできる方法があったら知りたい……。

1
2
3
4
5
6
7
8
#[derive(thiserror::Error, Debug)]
enum MyError<T> {
#[error("some application error has been occurred.")]
AppHoge,

#[error("aws api error has been occurred")]
Aws(#[from] aws_sdk_ec2::types::SdkError<T>),
}

Aws のエラーを2つに分けてみる

次に考えたのはこのパターンです。利用する AWS のサービスが増えるたびに追加が必要だけど、そこまで追加することはないからまぁいいか、という感覚です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("some application error has been occurred.")]
AppHoge,

#[error("aws api error has been occurred")]
AwsEc2(#[from] aws_sdk_ec2::Error),

#[error("aws api error has been occurred")]
AwsS3(#[from] aws_sdk_s3::Error),
}

impl<T> From<aws_sdk_ec2::types::SdkError<T>> for MyError
where
aws_sdk_ec2::types::SdkError<T>: Into<aws_sdk_ec2::Error>,
{
fn from(e: aws_sdk_ec2::types::SdkError<T>) -> Self {
MyError::Aws(e.into())
}
}

impl<T> From<aws_sdk_s3::types::SdkError<T>> for MyError
where
aws_sdk_s3::types::SdkError<T>: Into<aws_sdk_s3::Error>,
{
fn from(e: aws_sdk_s3::types::SdkError<T>) -> Self {
MyError::Aws(e.into())
}
}

// main は省略

ところがこれはダメです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
error[E0119]: conflicting implementations of trait `std::convert::From<aws_sdk_ec2::types::SdkError<_>>` for type `MyError`
--> src/main.rs:22:1
|
13 | / impl<T> From<aws_sdk_ec2::types::SdkError<T>> for MyError
14 | | where
15 | | aws_sdk_ec2::types::SdkError<T>: Into<aws_sdk_ec2::Error>,
16 | | {
... |
19 | | }
20 | | }
| |_- first implementation here
21 |
22 | / impl<T> From<aws_sdk_s3::types::SdkError<T>> for MyError
23 | | where
24 | | aws_sdk_s3::types::SdkError<T>: Into<aws_sdk_s3::Error>,
25 | | {
... |
28 | | }
29 | | }
| |_^ conflicting implementation for `MyError`

これがどうコンフリクトしているのかいまだに理解できていないです。aws_sdk_ec2::types::SdkError<T>aws_sdk_s3::types::SdkError<T> が同じものを指していることまではわかりますが、aws_sdk_ec2::Erroraws_sdk_s3::Error は別物なのでコンフリクトしなそうに思えてしまっています。

この状態から長いことガチャガチャいじってみたけどうまく解決しなかったので次の作戦に移行しました。

Box にぶち込む

これがひとまずの最終形です。AWS 系エラーは1つにまとめましたが、内部の型を残すのをあきらめました。これなら AWS のサービスが増えても何も変える必要がないです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[derive(thiserror::Error, Debug)]
enum MyError {
#[error("some application error has been occurred.")]
AppHoge,

#[error("aws api error has been occurred")]
Aws(Box<dyn std::error::Error + Send + Sync + 'static>),
}

impl<T> From<aws_sdk_ec2::types::SdkError<T>> for MyError
where
T: std::error::Error + Send + Sync + 'static,
{
fn from(e: aws_sdk_ec2::types::SdkError<T>) -> Self {
MyError::Aws(Box::new(e))
}
}

// main は省略

もしもエラーから具体的なものが必要になったら以下のようにダウンキャストもできるのでよしとしておきましょう。

1
2
3
4
5
if let MyError::Aws(e) = error {
if let Some(e) = e.downcast_ref::<aws_sdk_ec2::types::SdkError<DescribeInstancesError>>() {
// do something
}
}