Revise unhandled error variant according to RFC-39 (#3191)

This PR implements
[RFC-39](https://github.com/smithy-lang/smithy-rs/blob/main/design/src/rfcs/rfc0039_forward_compatible_errors.md)
with a couple slight deviations:
- No `introspect` method is added since `Error` already implements
`ProvideErrorMetadata`.
- The same opaqueness and deprecation pointer is applied to the enum
unknown variant for consistency.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
This commit is contained in:
John DiSanti 2023-11-15 10:12:13 -08:00 committed by GitHub
parent c0f72fbfe8
commit c830caa281
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 292 additions and 81 deletions

View File

@ -119,3 +119,61 @@ message = "Remove deprecated error kind type aliases."
references = ["smithy-rs#3189"] references = ["smithy-rs#3189"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" } meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti" author = "jdisanti"
[[aws-sdk-rust]]
message = """
Unhandled errors have been made opaque to ensure code is written in a future-proof manner. Where previously, you
might have:
```rust
match service_error.err() {
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
GetStorageError::Unhandled(unhandled) if unhandled.code() == Some("SomeUnmodeledErrorCode") {
// unhandled error handling
}
_ => { /* ... */ }
}
```
It should now look as follows:
```rust
match service_error.err() {
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
err if err.code() == Some("SomeUnmodeledErrorCode") {
// unhandled error handling
}
_ => { /* ... */ }
}
```
The `Unhandled` variant should never be referenced directly.
"""
references = ["smithy-rs#3191"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"
[[smithy-rs]]
message = """
Unhandled errors have been made opaque to ensure code is written in a future-proof manner. Where previously, you
might have:
```rust
match service_error.err() {
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
GetStorageError::Unhandled(unhandled) if unhandled.code() == Some("SomeUnmodeledErrorCode") {
// unhandled error handling
}
_ => { /* ... */ }
}
```
It should now look as follows:
```rust
match service_error.err() {
GetStorageError::StorageAccessNotAuthorized(_) => { /* ... */ }
err if err.code() == Some("SomeUnmodeledErrorCode") {
// unhandled error handling
}
_ => { /* ... */ }
}
```
The `Unhandled` variant should never be referenced directly.
"""
references = ["smithy-rs#3191"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti"

View File

@ -8,6 +8,7 @@ use aws_smithy_runtime_api::http::{Headers, Response};
use aws_smithy_types::error::metadata::{ use aws_smithy_types::error::metadata::{
Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata, Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata,
}; };
#[allow(deprecated)]
use aws_smithy_types::error::Unhandled; use aws_smithy_types::error::Unhandled;
const EXTENDED_REQUEST_ID: &str = "s3_extended_request_id"; const EXTENDED_REQUEST_ID: &str = "s3_extended_request_id";
@ -36,6 +37,7 @@ impl RequestIdExt for ErrorMetadata {
} }
} }
#[allow(deprecated)]
impl RequestIdExt for Unhandled { impl RequestIdExt for Unhandled {
fn extended_request_id(&self) -> Option<&str> { fn extended_request_id(&self) -> Option<&str> {
self.meta().extended_request_id() self.meta().extended_request_id()

View File

@ -11,6 +11,8 @@ use aws_smithy_runtime_api::http::Response;
use aws_smithy_types::error::metadata::{ use aws_smithy_types::error::metadata::{
Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata, Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata,
}; };
#[allow(deprecated)]
use aws_smithy_types::error::Unhandled; use aws_smithy_types::error::Unhandled;
/// Constant for the [`ErrorMetadata`] extra field that contains the request ID /// Constant for the [`ErrorMetadata`] extra field that contains the request ID
@ -38,6 +40,7 @@ impl RequestId for ErrorMetadata {
} }
} }
#[allow(deprecated)]
impl RequestId for Unhandled { impl RequestId for Unhandled {
fn request_id(&self) -> Option<&str> { fn request_id(&self) -> Option<&str> {
self.meta().request_id() self.meta().request_id()

View File

@ -146,7 +146,7 @@ abstract class BaseRequestIdDecorator : ClientCodegenDecorator {
val sym = codegenContext.symbolProvider.toSymbol(error) val sym = codegenContext.symbolProvider.toSymbol(error)
rust("Self::${sym.name}(e) => #T,", wrapped) rust("Self::${sym.name}(e) => #T,", wrapped)
} }
rust("Self::Unhandled(e) => e.$accessorFunctionName(),") rust("Self::Unhandled(e) => e.meta.$accessorFunctionName(),")
} }
} }
} }

View File

@ -9,6 +9,7 @@ use aws_sdk_lambda::operation::RequestId;
use aws_sdk_lambda::{Client, Config}; use aws_sdk_lambda::{Client, Config};
use aws_smithy_runtime::client::http::test_util::infallible_client_fn; use aws_smithy_runtime::client::http::test_util::infallible_client_fn;
#[allow(deprecated)]
async fn run_test( async fn run_test(
response: impl Fn() -> http::Response<&'static str> + Send + Sync + 'static, response: impl Fn() -> http::Response<&'static str> + Send + Sync + 'static,
expect_error: bool, expect_error: bool,

View File

@ -7,6 +7,7 @@
use aws_credential_types::provider::SharedCredentialsProvider; use aws_credential_types::provider::SharedCredentialsProvider;
use aws_sdk_s3::config::{Credentials, Region}; use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error;
use aws_sdk_s3::{Client, Config}; use aws_sdk_s3::{Client, Config};
use aws_smithy_runtime::client::http::test_util::capture_request; use aws_smithy_runtime::client::http::test_util::capture_request;
@ -58,8 +59,8 @@ async fn test_s3_signer_query_string_with_all_valid_chars() {
// test must be run against an actual bucket so we `ignore` it unless the runner specifically requests it // test must be run against an actual bucket so we `ignore` it unless the runner specifically requests it
#[tokio::test] #[tokio::test]
#[ignore] #[ignore]
#[allow(deprecated)]
async fn test_query_strings_are_correctly_encoded() { async fn test_query_strings_are_correctly_encoded() {
use aws_sdk_s3::operation::list_objects_v2::ListObjectsV2Error;
use aws_smithy_runtime_api::client::result::SdkError; use aws_smithy_runtime_api::client::result::SdkError;
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
@ -80,22 +81,19 @@ async fn test_query_strings_are_correctly_encoded() {
.send() .send()
.await; .await;
if let Err(SdkError::ServiceError(context)) = res { if let Err(SdkError::ServiceError(context)) = res {
match context.err() { let err = context.err();
ListObjectsV2Error::Unhandled(e) let msg = err.to_string();
if e.to_string().contains("SignatureDoesNotMatch") => let unhandled = matches!(err, ListObjectsV2Error::Unhandled(_));
{ if unhandled && msg.contains("SignatureDoesNotMatch") {
chars_that_break_signing.push(byte); chars_that_break_signing.push(byte);
} } else if unhandled && msg.to_string().contains("InvalidUri") {
ListObjectsV2Error::Unhandled(e) if e.to_string().contains("InvalidUri") => { chars_that_break_uri_parsing.push(byte);
chars_that_break_uri_parsing.push(byte); } else if unhandled && msg.to_string().contains("InvalidArgument") {
} chars_that_are_invalid_arguments.push(byte);
ListObjectsV2Error::Unhandled(e) if e.to_string().contains("InvalidArgument") => { } else if unhandled && msg.to_string().contains("InvalidToken") {
chars_that_are_invalid_arguments.push(byte); panic!("refresh your credentials and run this test again");
} } else {
ListObjectsV2Error::Unhandled(e) if e.to_string().contains("InvalidToken") => { todo!("unexpected error: {:?}", err);
panic!("refresh your credentials and run this test again");
}
e => todo!("unexpected error: {:?}", e),
} }
} }
} }

View File

@ -59,6 +59,7 @@ async fn get_request_id_from_modeled_error() {
} }
#[tokio::test] #[tokio::test]
#[allow(deprecated)]
async fn get_request_id_from_unmodeled_error() { async fn get_request_id_from_unmodeled_error() {
let (http_client, request) = capture_request(Some( let (http_client, request) = capture_request(Some(
http::Response::builder() http::Response::builder()

View File

@ -10,6 +10,7 @@ import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.core.rustlang.Visibility
import software.amazon.smithy.rust.codegen.core.rustlang.Writable import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.docs import software.amazon.smithy.rust.codegen.core.rustlang.docs
import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rust
@ -75,12 +76,39 @@ data class InfallibleEnumType(
) )
} }
override fun additionalEnumImpls(context: EnumGeneratorContext): Writable = writable {
// `try_parse` isn't needed for unnamed enums
if (context.enumTrait.hasNames()) {
rustTemplate(
"""
impl ${context.enumName} {
/// Parses the enum value while disallowing unknown variants.
///
/// Unknown variants will result in an error.
pub fn try_parse(value: &str) -> #{Result}<Self, #{UnknownVariantError}> {
match Self::from(value) {
##[allow(deprecated)]
Self::Unknown(_) => #{Err}(#{UnknownVariantError}::new(value)),
known => Ok(known),
}
}
}
""",
*preludeScope,
"UnknownVariantError" to unknownVariantError(),
)
}
}
override fun additionalDocs(context: EnumGeneratorContext): Writable = writable { override fun additionalDocs(context: EnumGeneratorContext): Writable = writable {
renderForwardCompatibilityNote(context.enumName, context.sortedMembers, UnknownVariant, UnknownVariantValue) renderForwardCompatibilityNote(context.enumName, context.sortedMembers, UnknownVariant, UnknownVariantValue)
} }
override fun additionalEnumMembers(context: EnumGeneratorContext): Writable = writable { override fun additionalEnumMembers(context: EnumGeneratorContext): Writable = writable {
docs("`$UnknownVariant` contains new variants that have been added since this code was generated.") docs("`$UnknownVariant` contains new variants that have been added since this code was generated.")
rust(
"""##[deprecated(note = "Don't directly match on `$UnknownVariant`. See the docs on this enum for the correct way to handle unknown variants.")]""",
)
rust("$UnknownVariant(#T)", unknownVariantValue(context)) rust("$UnknownVariant(#T)", unknownVariantValue(context))
} }
@ -93,10 +121,9 @@ data class InfallibleEnumType(
docs( docs(
""" """
Opaque struct used as inner data for the `Unknown` variant defined in enums in Opaque struct used as inner data for the `Unknown` variant defined in enums in
the crate the crate.
While this is not intended to be used directly, it is marked as `pub` because it is This is not intended to be used directly.
part of the enums that are public interface.
""".trimIndent(), """.trimIndent(),
) )
context.enumMeta.render(this) context.enumMeta.render(this)
@ -174,5 +201,35 @@ class ClientEnumGenerator(codegenContext: ClientCodegenContext, shape: StringSha
codegenContext.model, codegenContext.model,
codegenContext.symbolProvider, codegenContext.symbolProvider,
shape, shape,
InfallibleEnumType(ClientRustModule.primitives), InfallibleEnumType(
RustModule.new(
"sealed_enum_unknown",
visibility = Visibility.PUBCRATE,
parent = ClientRustModule.primitives,
),
),
) )
private fun unknownVariantError(): RuntimeType = RuntimeType.forInlineFun("UnknownVariantError", ClientRustModule.Error) {
rustTemplate(
"""
/// The given enum value failed to parse since it is not a known value.
##[derive(Debug)]
pub struct UnknownVariantError {
value: #{String},
}
impl UnknownVariantError {
pub(crate) fn new(value: impl #{Into}<#{String}>) -> Self {
Self { value: value.into() }
}
}
impl ::std::fmt::Display for UnknownVariantError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> #{Result}<(), ::std::fmt::Error> {
write!(f, "unknown enum variant: '{}'", self.value)
}
}
impl ::std::error::Error for UnknownVariantError {}
""",
*preludeScope,
)
}

View File

@ -28,7 +28,6 @@ import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.errorMetadata import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.errorMetadata
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.unhandledError
import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.core.smithy.customize.Section import software.amazon.smithy.rust.codegen.core.smithy.customize.Section
import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations
@ -83,12 +82,14 @@ class OperationErrorGenerator(
val errorVariantSymbol = symbolProvider.toSymbol(errorVariant) val errorVariantSymbol = symbolProvider.toSymbol(errorVariant)
write("${errorVariantSymbol.name}(#T),", errorVariantSymbol) write("${errorVariantSymbol.name}(#T),", errorVariantSymbol)
} }
rust( rustTemplate(
""" """
/// An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code). /// An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).
Unhandled(#T), #{deprecation}
Unhandled(#{Unhandled}),
""", """,
unhandledError(runtimeConfig), "deprecation" to writable { renderUnhandledErrorDeprecation(runtimeConfig, errorSymbol.name) },
"Unhandled" to unhandledError(runtimeConfig),
) )
} }
@ -114,15 +115,9 @@ class OperationErrorGenerator(
"StdError" to RuntimeType.StdError, "StdError" to RuntimeType.StdError,
"ErrorMeta" to errorMetadata, "ErrorMeta" to errorMetadata,
) { ) {
rust( rustTemplate(
""" """Self::Unhandled(#{Unhandled} { source, meta: meta.unwrap_or_default() })""",
Self::Unhandled({ "Unhandled" to unhandledError(runtimeConfig),
let mut builder = #T::builder().source(source);
builder.set_meta(meta);
builder.build()
})
""",
unhandledError(runtimeConfig),
) )
} }
} }
@ -131,8 +126,23 @@ class OperationErrorGenerator(
private fun RustWriter.renderImplDisplay(errorSymbol: Symbol, errors: List<StructureShape>) { private fun RustWriter.renderImplDisplay(errorSymbol: Symbol, errors: List<StructureShape>) {
rustBlock("impl #T for ${errorSymbol.name}", RuntimeType.Display) { rustBlock("impl #T for ${errorSymbol.name}", RuntimeType.Display) {
rustBlock("fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result") { rustBlock("fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result") {
delegateToVariants(errors) { delegateToVariants(errors) { variantMatch ->
writable { rust("_inner.fmt(f)") } when (variantMatch) {
is VariantMatch.Unhandled -> writable {
rustTemplate(
"""
if let #{Some}(code) = #{ProvideErrorMetadata}::code(self) {
write!(f, "unhandled error ({code})")
} else {
f.write_str("unhandled error")
}
""",
*preludeScope,
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(runtimeConfig),
)
}
is VariantMatch.Modeled -> writable { rust("_inner.fmt(f)") }
}
} }
} }
} }
@ -142,8 +152,13 @@ class OperationErrorGenerator(
val errorMetadataTrait = RuntimeType.provideErrorMetadataTrait(runtimeConfig) val errorMetadataTrait = RuntimeType.provideErrorMetadataTrait(runtimeConfig)
rustBlock("impl #T for ${errorSymbol.name}", errorMetadataTrait) { rustBlock("impl #T for ${errorSymbol.name}", errorMetadataTrait) {
rustBlock("fn meta(&self) -> &#T", errorMetadata(runtimeConfig)) { rustBlock("fn meta(&self) -> &#T", errorMetadata(runtimeConfig)) {
delegateToVariants(errors) { delegateToVariants(errors) { variantMatch ->
writable { rust("#T::meta(_inner)", errorMetadataTrait) } writable {
when (variantMatch) {
is VariantMatch.Unhandled -> rust("&_inner.meta")
is VariantMatch.Modeled -> rust("#T::meta(_inner)", errorMetadataTrait)
}
}
} }
} }
} }
@ -189,16 +204,16 @@ class OperationErrorGenerator(
""" """
/// Creates the `${errorSymbol.name}::Unhandled` variant from any error type. /// Creates the `${errorSymbol.name}::Unhandled` variant from any error type.
pub fn unhandled(err: impl #{Into}<#{Box}<dyn #{StdError} + #{Send} + #{Sync} + 'static>>) -> Self { pub fn unhandled(err: impl #{Into}<#{Box}<dyn #{StdError} + #{Send} + #{Sync} + 'static>>) -> Self {
Self::Unhandled(#{Unhandled}::builder().source(err).build()) Self::Unhandled(#{Unhandled} { source: err.into(), meta: #{Default}::default() })
} }
/// Creates the `${errorSymbol.name}::Unhandled` variant from a `#{error_metadata}`. /// Creates the `${errorSymbol.name}::Unhandled` variant from an [`ErrorMetadata`](#{ErrorMetadata}).
pub fn generic(err: #{error_metadata}) -> Self { pub fn generic(err: #{ErrorMetadata}) -> Self {
Self::Unhandled(#{Unhandled}::builder().source(err.clone()).meta(err).build()) Self::Unhandled(#{Unhandled} { source: err.clone().into(), meta: err })
} }
""", """,
*preludeScope, *preludeScope,
"error_metadata" to errorMetadata, "ErrorMetadata" to errorMetadata,
"StdError" to RuntimeType.StdError, "StdError" to RuntimeType.StdError,
"Unhandled" to unhandledError(runtimeConfig), "Unhandled" to unhandledError(runtimeConfig),
) )
@ -209,13 +224,15 @@ class OperationErrorGenerator(
""", """,
) )
rustBlock("pub fn meta(&self) -> &#T", errorMetadata) { rustBlock("pub fn meta(&self) -> &#T", errorMetadata) {
rust("use #T;", RuntimeType.provideErrorMetadataTrait(runtimeConfig))
rustBlock("match self") { rustBlock("match self") {
errors.forEach { error -> errors.forEach { error ->
val errorVariantSymbol = symbolProvider.toSymbol(error) val errorVariantSymbol = symbolProvider.toSymbol(error)
rust("Self::${errorVariantSymbol.name}(e) => e.meta(),") rustTemplate(
"Self::${errorVariantSymbol.name}(e) => #{ProvideErrorMetadata}::meta(e),",
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(runtimeConfig),
)
} }
rust("Self::Unhandled(e) => e.meta(),") rust("Self::Unhandled(e) => &e.meta,")
} }
} }
errors.forEach { error -> errors.forEach { error ->
@ -236,9 +253,14 @@ class OperationErrorGenerator(
*preludeScope, *preludeScope,
"StdError" to RuntimeType.StdError, "StdError" to RuntimeType.StdError,
) { ) {
delegateToVariants(errors) { delegateToVariants(errors) { variantMatch ->
writable { when (variantMatch) {
rustTemplate("#{Some}(_inner)", *preludeScope) is VariantMatch.Unhandled -> writable {
rustTemplate("#{Some}(&*_inner.source)", *preludeScope)
}
is VariantMatch.Modeled -> writable {
rustTemplate("#{Some}(_inner)", *preludeScope)
}
} }
} }
} }

View File

@ -9,6 +9,7 @@ import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata import software.amazon.smithy.rust.codegen.core.rustlang.RustMetadata
import software.amazon.smithy.rust.codegen.core.rustlang.RustModule import software.amazon.smithy.rust.codegen.core.rustlang.RustModule
@ -24,9 +25,9 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.rustlang.writable import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext import software.amazon.smithy.rust.codegen.core.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.preludeScope
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType.Companion.unhandledError
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations import software.amazon.smithy.rust.codegen.core.smithy.customize.writeCustomizations
import software.amazon.smithy.rust.codegen.core.smithy.generators.operationBuildError import software.amazon.smithy.rust.codegen.core.smithy.generators.operationBuildError
@ -91,7 +92,7 @@ class ServiceErrorGenerator(
allErrors.forEach { allErrors.forEach {
rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.source(),") rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.source(),")
} }
rust("Error::Unhandled(inner) => inner.source()") rustTemplate("Error::Unhandled(inner) => #{Some}(&*inner.source)", *preludeScope)
} }
} }
} }
@ -107,7 +108,17 @@ class ServiceErrorGenerator(
allErrors.forEach { allErrors.forEach {
rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.fmt(f),") rust("Error::${symbolProvider.toSymbol(it).name}(inner) => inner.fmt(f),")
} }
rust("Error::Unhandled(inner) => inner.fmt(f)") rustTemplate(
"""
Error::Unhandled(_) => if let #{Some}(code) = #{ProvideErrorMetadata}::code(self) {
write!(f, "unhandled error ({code})")
} else {
f.write_str("unhandled error")
}
""",
*preludeScope,
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(codegenContext.runtimeConfig),
)
} }
} }
} }
@ -118,11 +129,12 @@ class ServiceErrorGenerator(
""" """
impl From<#{BuildError}> for Error { impl From<#{BuildError}> for Error {
fn from(value: #{BuildError}) -> Self { fn from(value: #{BuildError}) -> Self {
Error::Unhandled(#{Unhandled}::builder().source(value).build()) Error::Unhandled(#{Unhandled} { source: value.into(), meta: #{Default}::default() })
} }
} }
""", """,
*preludeScope,
"BuildError" to codegenContext.runtimeConfig.operationBuildError(), "BuildError" to codegenContext.runtimeConfig.operationBuildError(),
"Unhandled" to unhandledError(codegenContext.runtimeConfig), "Unhandled" to unhandledError(codegenContext.runtimeConfig),
) )
@ -146,10 +158,10 @@ class ServiceErrorGenerator(
rustTemplate( rustTemplate(
""" """
_ => Error::Unhandled( _ => Error::Unhandled(
#{Unhandled}::builder() #{Unhandled} {
.meta(#{ProvideErrorMetadata}::meta(&err).clone()) meta: #{ProvideErrorMetadata}::meta(&err).clone(),
.source(err) source: err.into(),
.build() }
), ),
""", """,
"Unhandled" to unhandledError(codegenContext.runtimeConfig), "Unhandled" to unhandledError(codegenContext.runtimeConfig),
@ -187,7 +199,7 @@ class ServiceErrorGenerator(
fn meta(&self) -> &#{ErrorMetadata} { fn meta(&self) -> &#{ErrorMetadata} {
match self { match self {
#{matchers} #{matchers}
Self::Unhandled(inner) => inner.meta(), Self::Unhandled(inner) => &inner.meta,
} }
} }
} }
@ -220,7 +232,57 @@ class ServiceErrorGenerator(
rust("${sym.name}(#T),", sym) rust("${sym.name}(#T),", sym)
} }
docs("An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).") docs("An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).")
renderUnhandledErrorDeprecation(codegenContext.runtimeConfig, "Error")
rust("Unhandled(#T)", unhandledError(codegenContext.runtimeConfig)) rust("Unhandled(#T)", unhandledError(codegenContext.runtimeConfig))
} }
} }
} }
fun unhandledError(rc: RuntimeConfig): RuntimeType = RuntimeType.forInlineFun(
"Unhandled",
// Place in a sealed module so that it can't be referenced at all
RustModule.pubCrate("sealed_unhandled", ClientRustModule.Error),
) {
rustTemplate(
"""
/// This struct is not intended to be used.
///
/// This struct holds information about an unhandled error,
/// but that information should be obtained by using the
/// [`ProvideErrorMetadata`](#{ProvideErrorMetadata}) trait
/// on the error type.
///
/// This struct intentionally doesn't yield any useful information itself.
#{deprecation}
##[derive(Debug)]
pub struct Unhandled {
pub(crate) source: #{BoxError},
pub(crate) meta: #{ErrorMetadata},
}
""",
"BoxError" to RuntimeType.smithyRuntimeApi(rc).resolve("box_error::BoxError"),
"deprecation" to writable { renderUnhandledErrorDeprecation(rc) },
"ErrorMetadata" to RuntimeType.smithyTypes(rc).resolve("error::metadata::ErrorMetadata"),
"ProvideErrorMetadata" to RuntimeType.smithyTypes(rc).resolve("error::metadata::ProvideErrorMetadata"),
)
}
fun RustWriter.renderUnhandledErrorDeprecation(rc: RuntimeConfig, errorName: String? = null) {
val link = if (errorName != null) {
"##impl-ProvideErrorMetadata-for-$errorName"
} else {
"#{ProvideErrorMetadata}"
}
val message = """
Matching `Unhandled` directly is not forwards compatible. Instead, match using a
variable wildcard pattern and check `.code()`:<br/>
&nbsp;&nbsp;&nbsp;`err if err.code() == Some("SpecificExceptionCode") => { /* handle the error */ }`<br/>
See [`ProvideErrorMetadata`]($link) for what information is available for the error.
""".trimIndent()
// `.dq()` doesn't quite do what we want here since we actually want a Rust multi-line string
val messageEscaped = message.replace("\"", "\\\"").replace("\n", " \\\n").replace("<br/>", "\n")
rustTemplate(
"""##[deprecated(note = "$messageEscaped")]""",
"ProvideErrorMetadata" to RuntimeType.provideErrorMetadataTrait(rc),
)
}

View File

@ -119,7 +119,10 @@ class ClientEnumGeneratorTest {
""" """
assert_eq!(SomeEnum::from("Unknown"), SomeEnum::UnknownValue); assert_eq!(SomeEnum::from("Unknown"), SomeEnum::UnknownValue);
assert_eq!(SomeEnum::from("UnknownValue"), SomeEnum::UnknownValue_); assert_eq!(SomeEnum::from("UnknownValue"), SomeEnum::UnknownValue_);
assert_eq!(SomeEnum::from("SomethingNew"), SomeEnum::Unknown(crate::primitives::UnknownVariantValue("SomethingNew".to_owned()))); assert_eq!(
SomeEnum::from("SomethingNew"),
SomeEnum::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue("SomethingNew".to_owned()))
);
""", """,
) )
} }
@ -150,7 +153,10 @@ class ClientEnumGeneratorTest {
assert_eq!(instance.as_str(), "t2.micro"); assert_eq!(instance.as_str(), "t2.micro");
assert_eq!(InstanceType::from("t2.nano"), InstanceType::T2Nano); assert_eq!(InstanceType::from("t2.nano"), InstanceType::T2Nano);
// round trip unknown variants: // round trip unknown variants:
assert_eq!(InstanceType::from("other"), InstanceType::Unknown(crate::primitives::UnknownVariantValue("other".to_owned()))); assert_eq!(
InstanceType::from("other"),
InstanceType::Unknown(crate::primitives::sealed_enum_unknown::UnknownVariantValue("other".to_owned()))
);
assert_eq!(InstanceType::from("other").as_str(), "other"); assert_eq!(InstanceType::from("other").as_str(), "other");
""", """,
) )

View File

@ -51,7 +51,7 @@ class ClientEventStreamUnmarshallerGeneratorTest {
let result = $generator::new().unmarshall(&message); let result = $generator::new().unmarshall(&message);
assert!(result.is_ok(), "expected ok, got: {:?}", result); assert!(result.is_ok(), "expected ok, got: {:?}", result);
match expect_error(result.unwrap()) { match expect_error(result.unwrap()) {
TestStreamError::Unhandled(err) => { err @ TestStreamError::Unhandled(_) => {
let message = format!("{}", crate::error::DisplayErrorContext(&err)); let message = format!("{}", crate::error::DisplayErrorContext(&err));
let expected = "message: \"unmodeled error\""; let expected = "message: \"unmodeled error\"";
assert!(message.contains(expected), "Expected '{message}' to contain '{expected}'"); assert!(message.contains(expected), "Expected '{message}' to contain '{expected}'");

View File

@ -425,7 +425,6 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
fun provideErrorMetadataTrait(runtimeConfig: RuntimeConfig) = fun provideErrorMetadataTrait(runtimeConfig: RuntimeConfig) =
smithyTypes(runtimeConfig).resolve("error::metadata::ProvideErrorMetadata") smithyTypes(runtimeConfig).resolve("error::metadata::ProvideErrorMetadata")
fun unhandledError(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("error::Unhandled")
fun jsonErrors(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.jsonErrors(runtimeConfig)) fun jsonErrors(runtimeConfig: RuntimeConfig) = forInlineDependency(InlineDependency.jsonErrors(runtimeConfig))
fun awsQueryCompatibleErrors(runtimeConfig: RuntimeConfig) = fun awsQueryCompatibleErrors(runtimeConfig: RuntimeConfig) =
forInlineDependency(InlineDependency.awsQueryCompatibleErrors(runtimeConfig)) forInlineDependency(InlineDependency.awsQueryCompatibleErrors(runtimeConfig))

View File

@ -2,21 +2,20 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
/// Copyright © 2023, Amazon, LLC.
/// //! This example demonstrates how to handle service generated errors.
/// This example demonstrates how to handle service generated errors. //!
/// //! The example assumes that the Pokémon service is running on the localhost on TCP port 13734.
/// The example assumes that the Pokémon service is running on the localhost on TCP port 13734. //! Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md)
/// Refer to the [README.md](https://github.com/smithy-lang/smithy-rs/tree/main/examples/pokemon-service-client-usage/README.md) //! file for instructions on how to launch the service locally.
/// file for instructions on how to launch the service locally. //!
/// //! The example can be run using `cargo run --example handling-errors`.
/// The example can be run using `cargo run --example handling-errors`.
/// use pokemon_service_client::error::DisplayErrorContext;
use pokemon_service_client::Client as PokemonClient;
use pokemon_service_client::{error::SdkError, operation::get_storage::GetStorageError}; use pokemon_service_client::{error::SdkError, operation::get_storage::GetStorageError};
use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL}; use pokemon_service_client_usage::{setup_tracing_subscriber, POKEMON_SERVICE_URL};
use pokemon_service_client::Client as PokemonClient;
/// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734. /// Creates a new `smithy-rs` client that is configured to communicate with a locally running Pokémon service on TCP port 13734.
/// ///
/// # Examples /// # Examples
@ -77,14 +76,10 @@ async fn main() {
GetStorageError::ValidationError(ve) => { GetStorageError::ValidationError(ve) => {
tracing::error!(error = %ve, "A required field has not been set."); tracing::error!(error = %ve, "A required field has not been set.");
} }
// An unexpected error occurred (e.g., invalid JSON returned by the service or an unknown error code).
GetStorageError::Unhandled(uh) => {
tracing::error!(error = %uh, "An unhandled error has occurred.")
}
// The SdkError is marked as `#[non_exhaustive]`. Therefore, a catch-all pattern is required to handle // The SdkError is marked as `#[non_exhaustive]`. Therefore, a catch-all pattern is required to handle
// potential future variants introduced in SdkError. // potential future variants introduced in SdkError.
_ => { _ => {
tracing::error!(error = %se.err(), "Some other error has occurred on the server") tracing::error!(error = %DisplayErrorContext(se.err()), "Some other error has occurred on the server")
} }
} }
} }

View File

@ -8,5 +8,6 @@ allowed_external_types = [
"http::header::value::HeaderValue", "http::header::value::HeaderValue",
"http::request::Request", "http::request::Request",
"http::response::Response", "http::response::Response",
"http::status::StatusCode",
"http::uri::Uri", "http::uri::Uri",
] ]

View File

@ -13,4 +13,4 @@ mod response;
pub use error::HttpError; pub use error::HttpError;
pub use headers::{HeaderValue, Headers, HeadersIter}; pub use headers::{HeaderValue, Headers, HeadersIter};
pub use request::{Request, RequestParts}; pub use request::{Request, RequestParts};
pub use response::Response; pub use response::{Response, StatusCode};

View File

@ -13,6 +13,8 @@ pub mod operation;
mod unhandled; mod unhandled;
pub use metadata::ErrorMetadata; pub use metadata::ErrorMetadata;
#[allow(deprecated)]
pub use unhandled::Unhandled; pub use unhandled::Unhandled;
#[derive(Debug)] #[derive(Debug)]

View File

@ -3,11 +3,14 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
#![allow(deprecated)]
//! Unhandled error type. //! Unhandled error type.
use crate::error::{metadata::ProvideErrorMetadata, ErrorMetadata}; use crate::error::{metadata::ProvideErrorMetadata, ErrorMetadata};
use std::error::Error as StdError; use std::error::Error as StdError;
#[deprecated(note = "The `Unhandled` type is no longer used by errors.")]
/// Builder for [`Unhandled`] /// Builder for [`Unhandled`]
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Builder { pub struct Builder {
@ -58,6 +61,7 @@ impl Builder {
/// [`DisplayErrorContext`](crate::error::display::DisplayErrorContext), use another /// [`DisplayErrorContext`](crate::error::display::DisplayErrorContext), use another
/// error reporter library that visits the error's cause/source chain, or call /// error reporter library that visits the error's cause/source chain, or call
/// [`Error::source`](std::error::Error::source) for more details about the underlying cause. /// [`Error::source`](std::error::Error::source) for more details about the underlying cause.
#[deprecated(note = "This type is no longer used by errors.")]
#[derive(Debug)] #[derive(Debug)]
pub struct Unhandled { pub struct Unhandled {
source: Box<dyn StdError + Send + Sync + 'static>, source: Box<dyn StdError + Send + Sync + 'static>,