diff --git a/aws/rust-runtime/aws-http/src/user_agent.rs b/aws/rust-runtime/aws-http/src/user_agent.rs index a9f39dfbf92e072d79bc5fdc953077cdce920421..789fa544de13a75b29999edfb3859049a6b7d07b 100644 --- a/aws/rust-runtime/aws-http/src/user_agent.rs +++ b/aws/rust-runtime/aws-http/src/user_agent.rs @@ -575,9 +575,8 @@ impl From for UserAgentStageError { } } -lazy_static::lazy_static! { - static ref X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); -} +#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this +const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); impl MapRequest for UserAgentStage { type Error = UserAgentStageError; @@ -593,10 +592,8 @@ impl MapRequest for UserAgentStage { .ok_or(UserAgentStageErrorKind::UserAgentMissing)?; req.headers_mut() .append(USER_AGENT, HeaderValue::try_from(ua.ua_header())?); - req.headers_mut().append( - X_AMZ_USER_AGENT.clone(), - HeaderValue::try_from(ua.aws_ua_header())?, - ); + req.headers_mut() + .append(X_AMZ_USER_AGENT, HeaderValue::try_from(ua.aws_ua_header())?); Ok(req) }) @@ -779,7 +776,7 @@ mod test { .get(USER_AGENT) .expect("UA header should be set"); req.headers() - .get(&*X_AMZ_USER_AGENT) + .get(&X_AMZ_USER_AGENT) .expect("UA header should be set"); } } diff --git a/aws/rust-runtime/aws-runtime/Cargo.toml b/aws/rust-runtime/aws-runtime/Cargo.toml index f2e4ab2bf974c50754ae364f7523193b3b713851..defbe83df9425d6ff5cd8e380fcad55df2f9ca48 100644 --- a/aws/rust-runtime/aws-runtime/Cargo.toml +++ b/aws/rust-runtime/aws-runtime/Cargo.toml @@ -8,11 +8,14 @@ license = "Apache-2.0" repository = "https://github.com/awslabs/smithy-rs" [dependencies] -aws-types = { path = "../aws-types" } aws-credential-types = { path = "../aws-credential-types" } +aws-http = { path = "../aws-http" } aws-sigv4 = { path = "../aws-sigv4" } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api" } +aws-smithy-types = { path = "../../../rust-runtime/aws-smithy-types" } +aws-types = { path = "../aws-types" } +http = "0.2.3" tracing = "0.1" [dev-dependencies] diff --git a/aws/rust-runtime/aws-runtime/external-types.toml b/aws/rust-runtime/aws-runtime/external-types.toml index 3d92860f168270eb8d7eb31cedd533cdcec28b24..90b77e92e010f41b84349bec83c5a89d4df62554 100644 --- a/aws/rust-runtime/aws-runtime/external-types.toml +++ b/aws/rust-runtime/aws-runtime/external-types.toml @@ -1,6 +1,9 @@ allowed_external_types = [ "aws_credential_types::*", "aws_sigv4::*", + "aws_smithy_http::body::SdkBody", "aws_smithy_runtime_api::*", "aws_types::*", + "http::request::Request", + "http::response::Response", ] diff --git a/aws/rust-runtime/aws-runtime/src/lib.rs b/aws/rust-runtime/aws-runtime/src/lib.rs index 4070e43e33dd29bcf5a4eecbd9c6df3018e08a82..73ac01863df3d1e1686581d4d1c67fc73dd3f590 100644 --- a/aws/rust-runtime/aws-runtime/src/lib.rs +++ b/aws/rust-runtime/aws-runtime/src/lib.rs @@ -18,3 +18,6 @@ pub mod auth; /// Supporting code for identity in the AWS SDK. pub mod identity; + +/// Supporting code for user agent headers in the AWS SDK. +pub mod user_agent; diff --git a/aws/rust-runtime/aws-runtime/src/user_agent.rs b/aws/rust-runtime/aws-runtime/src/user_agent.rs new file mode 100644 index 0000000000000000000000000000000000000000..efa8e3c0d50273eb11db94d15a3b1d94231b69cb --- /dev/null +++ b/aws/rust-runtime/aws-runtime/src/user_agent.rs @@ -0,0 +1,233 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +use aws_http::user_agent::{ApiMetadata, AwsUserAgent}; +use aws_smithy_runtime_api::client::interceptors::error::BoxError; +use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; +use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse}; +use aws_smithy_runtime_api::config_bag::ConfigBag; +use aws_types::app_name::AppName; +use aws_types::os_shim_internal::Env; +use http::header::{InvalidHeaderValue, USER_AGENT}; +use http::{HeaderName, HeaderValue}; +use std::borrow::Cow; +use std::fmt; + +#[allow(clippy::declare_interior_mutable_const)] // we will never mutate this +const X_AMZ_USER_AGENT: HeaderName = HeaderName::from_static("x-amz-user-agent"); + +#[derive(Debug)] +enum UserAgentInterceptorError { + MissingApiMetadata, + InvalidHeaderValue(InvalidHeaderValue), +} + +impl std::error::Error for UserAgentInterceptorError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidHeaderValue(source) => Some(source), + Self::MissingApiMetadata => None, + } + } +} + +impl fmt::Display for UserAgentInterceptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::InvalidHeaderValue(_) => "AwsUserAgent generated an invalid HTTP header value. This is a bug. Please file an issue.", + Self::MissingApiMetadata => "The UserAgentInterceptor requires ApiMetadata to be set before the request is made. This is a bug. Please file an issue.", + }) + } +} + +impl From for UserAgentInterceptorError { + fn from(err: InvalidHeaderValue) -> Self { + UserAgentInterceptorError::InvalidHeaderValue(err) + } +} + +/// Generates and attaches the AWS SDK's user agent to a HTTP request +#[non_exhaustive] +#[derive(Debug, Default)] +pub struct UserAgentInterceptor; + +impl UserAgentInterceptor { + /// Creates a new `UserAgentInterceptor` + pub fn new() -> Self { + UserAgentInterceptor + } +} + +fn header_values( + ua: &AwsUserAgent, +) -> Result<(HeaderValue, HeaderValue), UserAgentInterceptorError> { + // Pay attention to the extremely subtle difference between ua_header and aws_ua_header below... + Ok(( + HeaderValue::try_from(ua.ua_header())?, + HeaderValue::try_from(ua.aws_ua_header())?, + )) +} + +impl Interceptor for UserAgentInterceptor { + fn modify_before_signing( + &self, + context: &mut InterceptorContext, + cfg: &mut ConfigBag, + ) -> Result<(), BoxError> { + let api_metadata = cfg + .get::() + .ok_or(UserAgentInterceptorError::MissingApiMetadata)?; + + // Allow for overriding the user agent by an earlier interceptor (so, for example, + // tests can use `AwsUserAgent::for_tests()`) by attempting to grab one out of the + // config bag before creating one. + let ua: Cow<'_, AwsUserAgent> = cfg + .get::() + .map(Cow::Borrowed) + .unwrap_or_else(|| { + let mut ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata.clone()); + + let maybe_app_name = cfg.get::(); + if let Some(app_name) = maybe_app_name { + ua.set_app_name(app_name.clone()); + } + Cow::Owned(ua) + }); + + let headers = context.request_mut()?.headers_mut(); + let (user_agent, x_amz_user_agent) = header_values(&ua)?; + headers.append(USER_AGENT, user_agent); + headers.append(X_AMZ_USER_AGENT, x_amz_user_agent); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use aws_smithy_http::body::SdkBody; + use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext}; + use aws_smithy_runtime_api::config_bag::ConfigBag; + use aws_smithy_runtime_api::type_erasure::TypedBox; + use aws_smithy_types::error::display::DisplayErrorContext; + + fn expect_header<'a>( + context: &'a InterceptorContext, + header_name: &str, + ) -> &'a str { + context + .request() + .unwrap() + .headers() + .get(header_name) + .unwrap() + .to_str() + .unwrap() + } + + #[test] + fn test_overridden_ua() { + let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let mut config = ConfigBag::base(); + config.put(AwsUserAgent::for_tests()); + config.put(ApiMetadata::new("unused", "unused")); + + let interceptor = UserAgentInterceptor::new(); + interceptor + .modify_before_signing(&mut context, &mut config) + .unwrap(); + + let header = expect_header(&context, "user-agent"); + assert_eq!(AwsUserAgent::for_tests().ua_header(), header); + assert!(!header.contains("unused")); + + assert_eq!( + AwsUserAgent::for_tests().aws_ua_header(), + expect_header(&context, "x-amz-user-agent") + ); + } + + #[test] + fn test_default_ua() { + let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let api_metadata = ApiMetadata::new("some-service", "some-version"); + let mut config = ConfigBag::base(); + config.put(api_metadata.clone()); + + let interceptor = UserAgentInterceptor::new(); + interceptor + .modify_before_signing(&mut context, &mut config) + .unwrap(); + + let expected_ua = AwsUserAgent::new_from_environment(Env::real(), api_metadata); + assert!( + expected_ua.aws_ua_header().contains("some-service"), + "precondition" + ); + assert_eq!( + expected_ua.ua_header(), + expect_header(&context, "user-agent") + ); + assert_eq!( + expected_ua.aws_ua_header(), + expect_header(&context, "x-amz-user-agent") + ); + } + + #[test] + fn test_app_name() { + let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let api_metadata = ApiMetadata::new("some-service", "some-version"); + let mut config = ConfigBag::base(); + config.put(api_metadata.clone()); + config.put(AppName::new("my_awesome_app").unwrap()); + + let interceptor = UserAgentInterceptor::new(); + interceptor + .modify_before_signing(&mut context, &mut config) + .unwrap(); + + let app_value = "app/my_awesome_app"; + let header = expect_header(&context, "user-agent"); + assert!( + !header.contains(app_value), + "expected `{header}` to not contain `{app_value}`" + ); + + let header = expect_header(&context, "x-amz-user-agent"); + assert!( + header.contains(app_value), + "expected `{header}` to contain `{app_value}`" + ); + } + + #[test] + fn test_api_metadata_missing() { + let mut context = InterceptorContext::new(TypedBox::new("doesntmatter").erase()); + context.set_request(http::Request::builder().body(SdkBody::empty()).unwrap()); + + let mut config = ConfigBag::base(); + + let interceptor = UserAgentInterceptor::new(); + let error = format!( + "{}", + DisplayErrorContext( + &*interceptor + .modify_before_signing(&mut context, &mut config) + .expect_err("it should error") + ) + ); + assert!( + error.contains("This is a bug"), + "`{error}` should contain message `This is a bug`" + ); + } +} diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt index 01e017337206dfbd70f8e902ab4a42a0cb62b07f..843d01e22f605c5f85a5a22e66cce9e46a4fe9a8 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/SigV4AuthDecorator.kt @@ -70,14 +70,21 @@ private class AuthServiceRuntimePluginCustomization(codegenContext: ClientCodege } is ServiceRuntimePluginSection.AdditionalConfig -> { + section.putConfigValue(this) { + rustTemplate("#{SigningService}::from_static(self.handle.conf.signing_service())", *codegenScope) + } rustTemplate( """ - cfg.put(#{SigningService}::from_static(self.handle.conf.signing_service())); if let Some(region) = self.handle.conf.region() { - cfg.put(#{SigningRegion}::from(region.clone())); + #{put_signing_region} } """, *codegenScope, + "put_signing_region" to writable { + section.putConfigValue(this) { + rustTemplate("#{SigningRegion}::from(region.clone())", *codegenScope) + } + }, ) } diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt index 38ff9f132472db481a3f7ad1fda8ee781b22a893..0e83d2c99dab5a2aa70f909ab9bdf23017935750 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/UserAgentDecorator.kt @@ -10,6 +10,8 @@ import software.amazon.smithy.model.shapes.OperationShape 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.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginCustomization +import software.amazon.smithy.rust.codegen.client.smithy.generators.ServiceRuntimePluginSection import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig import software.amazon.smithy.rust.codegen.core.rustlang.Writable @@ -25,6 +27,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSectio import software.amazon.smithy.rust.codegen.core.smithy.customize.adhocCustomization import software.amazon.smithy.rust.codegen.core.util.dq import software.amazon.smithy.rust.codegen.core.util.expectTrait +import software.amazon.smithy.rust.codegen.core.util.letIf /** * Inserts a UserAgent configuration into the operation @@ -45,9 +48,17 @@ class UserAgentDecorator : ClientCodegenDecorator { operation: OperationShape, baseCustomizations: List, ): List { - return baseCustomizations + UserAgentFeature(codegenContext) + return baseCustomizations + UserAgentMutateOpRequest(codegenContext) } + override fun serviceRuntimePluginCustomizations( + codegenContext: ClientCodegenContext, + baseCustomizations: List, + ): List = + baseCustomizations.letIf(codegenContext.settings.codegenConfig.enableNewSmithyRuntime) { + it + listOf(AddApiMetadataIntoConfigBag(codegenContext)) + } + override fun extraSections(codegenContext: ClientCodegenContext): List { return listOf( adhocCustomization { section -> @@ -86,8 +97,26 @@ class UserAgentDecorator : ClientCodegenDecorator { } } - private class UserAgentFeature( - private val codegenContext: ClientCodegenContext, + private class AddApiMetadataIntoConfigBag(codegenContext: ClientCodegenContext) : + ServiceRuntimePluginCustomization() { + private val runtimeConfig = codegenContext.runtimeConfig + private val awsRuntime = AwsRuntimeType.awsRuntime(runtimeConfig) + + override fun section(section: ServiceRuntimePluginSection): Writable = writable { + if (section is ServiceRuntimePluginSection.AdditionalConfig) { + section.putConfigValue(this) { + rust("#T.clone()", ClientRustModule.Meta.toType().resolve("API_METADATA")) + } + section.registerInterceptor(runtimeConfig, this) { + rust("#T::new()", awsRuntime.resolve("user_agent::UserAgentInterceptor")) + } + } + } + } + + // TODO(enableNewSmithyRuntime): Remove this customization class + private class UserAgentMutateOpRequest( + codegenContext: ClientCodegenContext, ) : OperationCustomization() { private val runtimeConfig = codegenContext.runtimeConfig diff --git a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs index 1a50e2a8cd4abb6b3df6b673fd1f52d2dbf54594..c6c142458a869add7de0b83419ddebd4bfe204ee 100644 --- a/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs +++ b/aws/sra-test/integration-tests/aws-sdk-s3/tests/sra_test.rs @@ -5,8 +5,9 @@ use aws_credential_types::cache::{CredentialsCache, SharedCredentialsCache}; use aws_credential_types::provider::SharedCredentialsProvider; -use aws_http::user_agent::AwsUserAgent; +use aws_http::user_agent::{ApiMetadata, AwsUserAgent}; use aws_runtime::auth::sigv4::SigV4OperationSigningConfig; +use aws_runtime::user_agent::UserAgentInterceptor; use aws_sdk_s3::config::{Credentials, Region}; use aws_sdk_s3::operation::list_objects_v2::{ ListObjectsV2Error, ListObjectsV2Input, ListObjectsV2Output, @@ -16,9 +17,7 @@ use aws_smithy_client::erase::DynConnector; use aws_smithy_client::test_connection::TestConnection; use aws_smithy_runtime::client::connections::adapter::DynConnectorAdapter; use aws_smithy_runtime_api::client::endpoints::StaticUriEndpointResolver; -use aws_smithy_runtime_api::client::interceptors::{ - Interceptor, InterceptorContext, InterceptorError, Interceptors, -}; +use aws_smithy_runtime_api::client::interceptors::{Interceptor, InterceptorContext, Interceptors}; use aws_smithy_runtime_api::client::orchestrator::{ BoxError, ConfigBagAccessors, Connection, HttpRequest, HttpResponse, TraceProbe, }; @@ -27,7 +26,7 @@ use aws_smithy_runtime_api::config_bag::ConfigBag; use aws_smithy_runtime_api::type_erasure::TypedBox; use aws_types::region::SigningRegion; use aws_types::SigningService; -use http::{HeaderValue, Uri}; +use http::Uri; use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; @@ -134,24 +133,6 @@ async fn sra_manual_test() { cfg.put(SigningService::from_static("s3")); cfg.put(SigningRegion::from(Region::from_static("us-east-1"))); - #[derive(Debug)] - struct UserAgentInterceptor; - impl Interceptor for UserAgentInterceptor { - fn modify_before_signing( - &self, - context: &mut InterceptorContext, - _cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { - let ua = AwsUserAgent::for_tests(); - - context.request_mut().unwrap().headers_mut().append( - "x-amz-user-agent", - HeaderValue::from_str(&ua.aws_ua_header()).unwrap(), - ); - Ok(()) - } - } - #[derive(Debug)] struct OverrideSigningTimeInterceptor; impl Interceptor for OverrideSigningTimeInterceptor { @@ -159,7 +140,7 @@ async fn sra_manual_test() { &self, _context: &InterceptorContext, cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { + ) -> Result<(), BoxError> { let mut signing_config = cfg.get::().unwrap().clone(); signing_config.signing_options.request_timestamp = @@ -169,9 +150,11 @@ async fn sra_manual_test() { } } + cfg.put(ApiMetadata::new("unused", "unused")); + cfg.put(AwsUserAgent::for_tests()); // Override the user agent with the test UA cfg.get::>() .expect("interceptors set") - .register_client_interceptor(Arc::new(UserAgentInterceptor) as _) + .register_client_interceptor(Arc::new(UserAgentInterceptor::new()) as _) .register_client_interceptor(Arc::new(OverrideSigningTimeInterceptor) as _); Ok(()) } diff --git a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt index 52a6519b6542f396270a317a156fe29dfee2fb05..359bb32d1f4dd2548069e5a30051425d3d567cfa 100644 --- a/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt +++ b/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/generators/ServiceRuntimePluginGenerator.kt @@ -7,8 +7,11 @@ package software.amazon.smithy.rust.codegen.client.smithy.generators import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.rustlang.Writable +import software.amazon.smithy.rust.codegen.core.rustlang.rust 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.smithy.RuntimeConfig import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType import software.amazon.smithy.rust.codegen.core.smithy.customize.NamedCustomization import software.amazon.smithy.rust.codegen.core.smithy.customize.Section @@ -38,7 +41,28 @@ sealed class ServiceRuntimePluginSection(name: String) : Section(name) { /** * Hook for adding additional things to config inside service runtime plugins. */ - data class AdditionalConfig(val configBagName: String) : ServiceRuntimePluginSection("AdditionalConfig") + data class AdditionalConfig(val configBagName: String) : ServiceRuntimePluginSection("AdditionalConfig") { + /** Adds a value to the config bag */ + fun putConfigValue(writer: RustWriter, value: Writable) { + writer.rust("$configBagName.put(#T);", value) + } + + /** Generates the code to register an interceptor */ + fun registerInterceptor(runtimeConfig: RuntimeConfig, writer: RustWriter, interceptor: Writable) { + val smithyRuntimeApi = RuntimeType.smithyRuntimeApi(runtimeConfig) + writer.rustTemplate( + """ + $configBagName.get::<#{Interceptors}<#{HttpRequest}, #{HttpResponse}>>() + .expect("interceptors set") + .register_client_interceptor(std::sync::Arc::new(#{interceptor}) as _); + """, + "HttpRequest" to smithyRuntimeApi.resolve("client::orchestrator::HttpRequest"), + "HttpResponse" to smithyRuntimeApi.resolve("client::orchestrator::HttpResponse"), + "Interceptors" to smithyRuntimeApi.resolve("client::interceptors::Interceptors"), + "interceptor" to interceptor, + ) + } + } } typealias ServiceRuntimePluginCustomization = NamedCustomization diff --git a/rust-runtime/aws-smithy-runtime-api/Cargo.toml b/rust-runtime/aws-smithy-runtime-api/Cargo.toml index 68e2005e93a59887e86f20403c6ea63648735102..a03bac307bbb5fec1fbe892f99bf31c979ecc934 100644 --- a/rust-runtime/aws-smithy-runtime-api/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime-api/Cargo.toml @@ -14,6 +14,7 @@ aws-smithy-http = { path = "../aws-smithy-http" } aws-smithy-types = { path = "../aws-smithy-types" } http = "0.2.3" tokio = { version = "1.25", features = ["sync"] } +tracing = "0.1" [package.metadata.docs.rs] all-features = true diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs index c1734d58c24a0cfee2e7684b110fb2a15a221ffd..6312eea80f49fc524e4152a9baa4e671e8fed97e 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors.rs @@ -7,8 +7,9 @@ pub mod context; pub mod error; use crate::config_bag::ConfigBag; +use aws_smithy_types::error::display::DisplayErrorContext; pub use context::InterceptorContext; -pub use error::InterceptorError; +pub use error::{BoxError, InterceptorError}; use std::sync::{Arc, Mutex}; macro_rules! interceptor_trait_fn { @@ -18,7 +19,7 @@ macro_rules! interceptor_trait_fn { &self, context: &InterceptorContext, cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { + ) -> Result<(), BoxError> { let _ctx = context; let _cfg = cfg; Ok(()) @@ -30,7 +31,7 @@ macro_rules! interceptor_trait_fn { &self, context: &mut InterceptorContext, cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { + ) -> Result<(), BoxError> { let _ctx = context; let _cfg = cfg; Ok(()) @@ -544,8 +545,8 @@ pub type SharedInterceptor = Arc + S #[derive(Debug)] struct Inner { - client_interceptors: Vec + Send + Sync>>, - operation_interceptors: Vec + Send + Sync>>, + client_interceptors: Vec>, + operation_interceptors: Vec>, } // The compiler isn't smart enough to realize that TxReq and TxRes don't need to implement `Clone` @@ -591,41 +592,33 @@ macro_rules! interceptor_impl_fn { interceptor_impl_fn!(mut context, $name, $name); }; (context, $outer_name:ident, $inner_name:ident) => { - pub fn $outer_name( - &self, - context: &InterceptorContext, - cfg: &mut ConfigBag, - ) -> Result<(), InterceptorError> { - // Since interceptors can modify the interceptor list (since its in the config bag), copy the list ahead of time. - // This should be cheap since the interceptors inside the list are Arcs. - let client_interceptors = self.inner.lock().unwrap().client_interceptors.clone(); - for interceptor in client_interceptors { - interceptor.$inner_name(context, cfg)?; - } - let operation_interceptors = self.inner.lock().unwrap().operation_interceptors.clone(); - for interceptor in operation_interceptors { - interceptor.$inner_name(context, cfg)?; - } - Ok(()) - } + interceptor_impl_fn!( + $outer_name, + $inner_name(context: &InterceptorContext) + ); }; (mut context, $outer_name:ident, $inner_name:ident) => { + interceptor_impl_fn!( + $outer_name, + $inner_name(context: &mut InterceptorContext) + ); + }; + ($outer_name:ident, $inner_name:ident ($context:ident : $context_ty:ty)) => { pub fn $outer_name( &self, - context: &mut InterceptorContext, + $context: $context_ty, cfg: &mut ConfigBag, ) -> Result<(), InterceptorError> { - // Since interceptors can modify the interceptor list (since its in the config bag), copy the list ahead of time. - // This should be cheap since the interceptors inside the list are Arcs. - let client_interceptors = self.inner.lock().unwrap().client_interceptors.clone(); - for interceptor in client_interceptors { - interceptor.$inner_name(context, cfg)?; + let mut result: Result<(), BoxError> = Ok(()); + for interceptor in self.interceptors() { + if let Err(new_error) = interceptor.$inner_name($context, cfg) { + if let Err(last_error) = result { + tracing::debug!("{}", DisplayErrorContext(&*last_error)); + } + result = Err(new_error); + } } - let operation_interceptors = self.inner.lock().unwrap().operation_interceptors.clone(); - for interceptor in operation_interceptors { - interceptor.$inner_name(context, cfg)?; - } - Ok(()) + result.map_err(InterceptorError::$inner_name) } }; } @@ -635,6 +628,22 @@ impl Interceptors { Self::default() } + fn interceptors(&self) -> Vec> { + // Since interceptors can modify the interceptor list (since its in the config bag), copy the list ahead of time. + // This should be cheap since the interceptors inside the list are Arcs. + // TODO(enableNewSmithyRuntime): Remove the ability for interceptors to modify the interceptor list and then simplify this + let mut interceptors = self.inner.lock().unwrap().client_interceptors.clone(); + interceptors.extend( + self.inner + .lock() + .unwrap() + .operation_interceptors + .iter() + .cloned(), + ); + interceptors + } + pub fn register_client_interceptor( &self, interceptor: SharedInterceptor, diff --git a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs index fc30027718372d3d781fa2c41c5ca0144fd12e5d..e25b797aebbd170dc27de8b420a4a3a890cf020e 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/client/interceptors/error.rs @@ -3,14 +3,37 @@ * SPDX-License-Identifier: Apache-2.0 */ -//! Errors related to smithy interceptors +//! Errors related to Smithy interceptors use std::fmt; +macro_rules! interceptor_error_fn { + ($fn_name:ident => $error_kind:ident (with source)) => { + #[doc = concat!("Create a new error indicating a failure with a ", stringify!($fn_name), " interceptor.")] + pub fn $fn_name( + source: impl Into>, + ) -> Self { + Self { + kind: ErrorKind::$error_kind, + source: Some(source.into()), + } + } + }; + ($fn_name:ident => $error_kind:ident (invalid $thing:ident access)) => { + #[doc = concat!("Create a new error indicating that an interceptor tried to access the ", stringify!($thing), " out of turn.")] + pub fn $fn_name() -> Self { + Self { + kind: ErrorKind::$error_kind, + source: None, + } + } + } +} + /// A generic error that behaves itself in async contexts pub type BoxError = Box; -/// An error related to smithy interceptors. +/// An error related to Smithy interceptors. #[derive(Debug)] pub struct InterceptorError { kind: ErrorKind, @@ -18,205 +41,30 @@ pub struct InterceptorError { } impl InterceptorError { - /// Create a new error indicating a failure withing a read_before_execution interceptor - pub fn read_before_execution( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadBeforeExecution, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_serialization interceptor - pub fn modify_before_serialization( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeSerialization, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_before_serialization interceptor - pub fn read_before_serialization( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadBeforeSerialization, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_after_serialization interceptor - pub fn read_after_serialization( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadAfterSerialization, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_retry_loop interceptor - pub fn modify_before_retry_loop( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeRetryLoop, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_before_attempt interceptor - pub fn read_before_attempt( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadBeforeAttempt, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_signing interceptor - pub fn modify_before_signing( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeSigning, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_before_signing interceptor - pub fn read_before_signing( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadBeforeSigning, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_after_signing interceptor - pub fn read_after_signing( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadAfterSigning, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_transmit interceptor - pub fn modify_before_transmit( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeTransmit, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_before_transmit interceptor - pub fn read_before_transmit( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadBeforeTransmit, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_after_transmit interceptor - pub fn read_after_transmit( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadAfterTransmit, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_deserialization interceptor - pub fn modify_before_deserialization( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeDeserialization, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_before_deserialization interceptor - pub fn read_before_deserialization( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadBeforeDeserialization, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_after_deserialization interceptor - pub fn read_after_deserialization( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadAfterDeserialization, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_attempt_completion interceptor - pub fn modify_before_attempt_completion( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeAttemptCompletion, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_after_attempt interceptor - pub fn read_after_attempt( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadAfterAttempt, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a modify_before_completion interceptor - pub fn modify_before_completion( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ModifyBeforeCompletion, - source: Some(source.into()), - } - } - /// Create a new error indicating a failure withing a read_after_execution interceptor - pub fn read_after_execution( - source: impl Into>, - ) -> Self { - Self { - kind: ErrorKind::ReadAfterExecution, - source: Some(source.into()), - } - } - /// Create a new error indicating that an interceptor tried to access the request out of turn - pub fn invalid_request_access() -> Self { - Self { - kind: ErrorKind::InvalidRequestAccess, - source: None, - } - } - /// Create a new error indicating that an interceptor tried to access the response out of turn - pub fn invalid_response_access() -> Self { - Self { - kind: ErrorKind::InvalidResponseAccess, - source: None, - } - } - /// Create a new error indicating that an interceptor tried to access the input out of turn - pub fn invalid_input_access() -> Self { - Self { - kind: ErrorKind::InvalidInputAccess, - source: None, - } - } - /// Create a new error indicating that an interceptor tried to access the output out of turn - pub fn invalid_output_access() -> Self { - Self { - kind: ErrorKind::InvalidOutputAccess, - source: None, - } - } + interceptor_error_fn!(read_before_execution => ReadBeforeExecution (with source)); + interceptor_error_fn!(modify_before_serialization => ModifyBeforeSerialization (with source)); + interceptor_error_fn!(read_before_serialization => ReadBeforeSerialization (with source)); + interceptor_error_fn!(read_after_serialization => ReadAfterSerialization (with source)); + interceptor_error_fn!(modify_before_retry_loop => ModifyBeforeRetryLoop (with source)); + interceptor_error_fn!(read_before_attempt => ReadBeforeAttempt (with source)); + interceptor_error_fn!(modify_before_signing => ModifyBeforeSigning (with source)); + interceptor_error_fn!(read_before_signing => ReadBeforeSigning (with source)); + interceptor_error_fn!(read_after_signing => ReadAfterSigning (with source)); + interceptor_error_fn!(modify_before_transmit => ModifyBeforeTransmit (with source)); + interceptor_error_fn!(read_before_transmit => ReadBeforeTransmit (with source)); + interceptor_error_fn!(read_after_transmit => ReadAfterTransmit (with source)); + interceptor_error_fn!(modify_before_deserialization => ModifyBeforeDeserialization (with source)); + interceptor_error_fn!(read_before_deserialization => ReadBeforeDeserialization (with source)); + interceptor_error_fn!(read_after_deserialization => ReadAfterDeserialization (with source)); + interceptor_error_fn!(modify_before_attempt_completion => ModifyBeforeAttemptCompletion (with source)); + interceptor_error_fn!(read_after_attempt => ReadAfterAttempt (with source)); + interceptor_error_fn!(modify_before_completion => ModifyBeforeCompletion (with source)); + interceptor_error_fn!(read_after_execution => ReadAfterExecution (with source)); + + interceptor_error_fn!(invalid_request_access => InvalidRequestAccess (invalid request access)); + interceptor_error_fn!(invalid_response_access => InvalidResponseAccess (invalid response access)); + interceptor_error_fn!(invalid_input_access => InvalidInputAccess (invalid input access)); + interceptor_error_fn!(invalid_output_access => InvalidOutputAccess (invalid output access)); } #[derive(Debug)] @@ -259,7 +107,6 @@ enum ErrorKind { ModifyBeforeCompletion, /// An error occurred within the read_after_execution interceptor ReadAfterExecution, - // There is no InvalidModeledRequestAccess because it's always accessible /// An interceptor tried to access the request out of turn InvalidRequestAccess, /// An interceptor tried to access the response out of turn @@ -270,82 +117,51 @@ enum ErrorKind { InvalidOutputAccess, } -impl fmt::Display for InterceptorError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +macro_rules! display_interceptor_err { + ($self:ident, $f:ident, $(($error_kind:ident => $fn_name:ident ($($option:tt)+)),)+) => { + { use ErrorKind::*; - match &self.kind { - ReadBeforeExecution => { - write!(f, "read_before_execution interceptor encountered an error") - } - ModifyBeforeSerialization => write!( - f, - "modify_before_serialization interceptor encountered an error" - ), - ReadBeforeSerialization => write!( - f, - "read_before_serialization interceptor encountered an error" - ), - ReadAfterSerialization => write!( - f, - "read_after_serialization interceptor encountered an error" - ), - ModifyBeforeRetryLoop => write!( - f, - "modify_before_retry_loop interceptor encountered an error" - ), - ReadBeforeAttempt => write!(f, "read_before_attempt interceptor encountered an error"), - ModifyBeforeSigning => { - write!(f, "modify_before_signing interceptor encountered an error") - } - ReadBeforeSigning => write!(f, "read_before_signing interceptor encountered an error"), - ReadAfterSigning => write!(f, "read_after_signing interceptor encountered an error"), - ModifyBeforeTransmit => { - write!(f, "modify_before_transmit interceptor encountered an error") - } - ReadBeforeTransmit => { - write!(f, "read_before_transmit interceptor encountered an error") - } - ReadAfterTransmit => write!(f, "read_after_transmit interceptor encountered an error"), - ModifyBeforeDeserialization => write!( - f, - "modify_before_deserialization interceptor encountered an error" - ), - ReadBeforeDeserialization => write!( - f, - "read_before_deserialization interceptor encountered an error" - ), - ReadAfterDeserialization => write!( - f, - "read_after_deserialization interceptor encountered an error" - ), - ModifyBeforeAttemptCompletion => write!( - f, - "modify_before_attempt_completion interceptor encountered an error" - ), - ReadAfterAttempt => write!(f, "read_after_attempt interceptor encountered an error"), - ModifyBeforeCompletion => write!( - f, - "modify_before_completion interceptor encountered an error" - ), - ReadAfterExecution => { - write!(f, "read_after_execution interceptor encountered an error") - } - InvalidRequestAccess => { - write!(f, "tried to access request before request serialization") - } - InvalidResponseAccess => { - write!(f, "tried to access response before transmitting a request") - } - InvalidInputAccess => write!( - f, - "tried to access the input before response deserialization" - ), - InvalidOutputAccess => write!( - f, - "tried to access the output before response deserialization" - ), + match &$self.kind { + $($error_kind => display_interceptor_err!($f, $fn_name, ($($option)+)),)+ } } + }; + ($f:ident, $fn_name:ident, (interceptor error)) => { + $f.write_str(concat!(stringify!($fn_name), " interceptor encountered an error")) + }; + ($f:ident, $fn_name:ident, (invalid access $name:ident $message:literal)) => { + $f.write_str(concat!("tried to access the ", stringify!($name), " ", $message)) + }; +} + +impl fmt::Display for InterceptorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + display_interceptor_err!(self, f, + (ReadBeforeExecution => read_before_execution (interceptor error)), + (ModifyBeforeSerialization => modify_before_serialization (interceptor error)), + (ReadBeforeSerialization => read_before_serialization (interceptor error)), + (ReadAfterSerialization => read_after_serialization (interceptor error)), + (ModifyBeforeRetryLoop => modify_before_retry_loop (interceptor error)), + (ReadBeforeAttempt => read_Before_attempt (interceptor error)), + (ModifyBeforeSigning => modify_before_signing (interceptor error)), + (ReadBeforeSigning => read_before_signing (interceptor error)), + (ReadAfterSigning => read_after_signing (interceptor error)), + (ModifyBeforeTransmit => modify_before_transmit (interceptor error)), + (ReadBeforeTransmit => read_before_transmit (interceptor error)), + (ReadAfterTransmit => read_after_transmit (interceptor error)), + (ModifyBeforeDeserialization => modify_before_deserialization (interceptor error)), + (ReadBeforeDeserialization => read_before_deserialization (interceptor error)), + (ReadAfterDeserialization => read_after_deserialization (interceptor error)), + (ModifyBeforeAttemptCompletion => modify_before_attempt_completion (interceptor error)), + (ReadAfterAttempt => read_after_attempt (interceptor error)), + (ModifyBeforeCompletion => modify_before_completion (interceptor error)), + (ReadAfterExecution => read_after_execution (interceptor error)), + (InvalidRequestAccess => invalid_request_access (invalid access request "before request serialization")), + (InvalidResponseAccess => invalid_response_access (invalid access response "before transmitting a request")), + (InvalidInputAccess => invalid_input_access (invalid access input "after request serialization")), + (InvalidOutputAccess => invalid_output_access (invalid access output "before response deserialization")), + ) + } } impl std::error::Error for InterceptorError { diff --git a/rust-runtime/aws-smithy-runtime-api/src/config_bag.rs b/rust-runtime/aws-smithy-runtime-api/src/config_bag.rs index 78e7b2502e26b890044b890a894359d263b8b99e..bf8d42e6cba1c2562f49386a40f1ab8fd36787f0 100644 --- a/rust-runtime/aws-smithy-runtime-api/src/config_bag.rs +++ b/rust-runtime/aws-smithy-runtime-api/src/config_bag.rs @@ -10,7 +10,6 @@ //! 1. A new layer of configuration may be applied onto an existing configuration structure without modifying it or taking ownership. //! 2. No lifetime shenanigans to deal with use aws_smithy_http::property_bag::PropertyBag; -use std::any::type_name; use std::fmt::Debug; use std::ops::Deref; use std::sync::Arc; @@ -19,6 +18,7 @@ use std::sync::Arc; /// /// [`ConfigBag`] is the "unlocked" form of the bag. Only the top layer of the bag may be unlocked. #[must_use] +#[derive(Debug)] pub struct ConfigBag { head: Layer, tail: Option, @@ -27,7 +27,7 @@ pub struct ConfigBag { /// Layered Configuration Structure /// /// [`FrozenConfigBag`] is the "locked" form of the bag. -#[derive(Clone)] +#[derive(Clone, Debug)] #[must_use] pub struct FrozenConfigBag(Arc); @@ -55,6 +55,7 @@ enum Value { ExplicitlyUnset, } +#[derive(Debug)] struct Layer { name: &'static str, props: PropertyBag, @@ -122,7 +123,6 @@ impl ConfigBag { pub fn get(&self) -> Option<&T> { let mut source = vec![]; let out = self.sourced_get(&mut source); - println!("searching for {:?} {:#?}", type_name::(), source); out }