Skip to content

Commit

Permalink
Implement request ID access for SDK clients RFC (smithy-lang#2129)
Browse files Browse the repository at this point in the history
* Add `RequestId` trait
* Implement `RequestId` for generated AWS client errors
* Move `RustWriter.implBlock` out of `StructureGenerator`
* Create structure/builder customization hooks
* Customize `_request_id` into AWS outputs
* Set request ID on outputs
* Refactor SDK service decorators
* Refactor S3's extended request ID implementation
* Combine `Error` and `ErrorKind`
* Add test for service error conversion
* Move error generators into `codegen-client` and fix tests
* Re-export `ErrorMetadata`
* Add request IDs to trace logs
* Simplify some error trait handling
* Rename `ClientContextParamDecorator` to `ClientContextConfigCustomization`
* Add deprecated alias to guide customers through upgrading
* Rename the `ErrorMetadata` trait to `ProvideErrorMetadata`
* Rename `aws_smithy_types::Error` to `ErrorMetadata`
  • Loading branch information
jdisanti committed Feb 11, 2023
1 parent 0a11d51 commit d48878e
Show file tree
Hide file tree
Showing 102 changed files with 2,682 additions and 1,106 deletions.
164 changes: 164 additions & 0 deletions CHANGELOG.next.toml
Expand Up @@ -28,3 +28,167 @@ message = "Adds jitter to `LazyCredentialsCache`. This allows credentials with t
references = ["smithy-rs#2335"]
meta = { "breaking" = false, "tada" = false, "bug" = false }
author = "ysaito1001"

[[aws-sdk-rust]]
message = """Request IDs can now be easily retrieved on successful responses. For example, with S3:
```rust
// Import the trait to get the `request_id` method on outputs
use aws_sdk_s3::types::RequestId;
let output = client.list_buckets().send().await?;
println!("Request ID: {:?}", output.request_id());
```
"""
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"

[[aws-sdk-rust]]
message = """Retrieving a request ID from errors now requires importing the `RequestId` trait. For example, with S3:
```rust
use aws_sdk_s3::types::RequestId;
println!("Request ID: {:?}", error.request_id());
```
"""
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"

[[smithy-rs]]
message = "Generic clients no longer expose a `request_id()` function on errors. To get request ID functionality, use the SDK code generator."
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client"}
author = "jdisanti"

[[aws-sdk-rust]]
message = "The `message()` and `code()` methods on errors have been moved into `ProvideErrorMetadata` trait. This trait will need to be imported to continue calling these."
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"

[[smithy-rs]]
message = "The `message()` and `code()` methods on errors have been moved into `ProvideErrorMetadata` trait. This trait will need to be imported to continue calling these."
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client"}
author = "jdisanti"

[[aws-sdk-rust]]
message = """
The `*Error` and `*ErrorKind` types have been combined to make error matching simpler.
<details>
<summary>Example with S3</summary>
**Before:**
```rust
let result = client
.get_object()
.bucket(BUCKET_NAME)
.key("some-key")
.send()
.await;
match result {
Ok(_output) => { /* Do something with the output */ }
Err(err) => match err.into_service_error() {
GetObjectError { kind, .. } => match kind {
GetObjectErrorKind::InvalidObjectState(value) => println!("invalid object state: {:?}", value),
GetObjectErrorKind::NoSuchKey(_) => println!("object didn't exist"),
}
err @ GetObjectError { .. } if err.code() == Some("SomeUnmodeledError") => {}
err @ _ => return Err(err.into()),
},
}
```
**After:**
```rust
// Needed to access the `.code()` function on the error type:
use aws_sdk_s3::types::ProvideErrorMetadata;
let result = client
.get_object()
.bucket(BUCKET_NAME)
.key("some-key")
.send()
.await;
match result {
Ok(_output) => { /* Do something with the output */ }
Err(err) => match err.into_service_error() {
GetObjectError::InvalidObjectState(value) => {
println!("invalid object state: {:?}", value);
}
GetObjectError::NoSuchKey(_) => {
println!("object didn't exist");
}
err if err.code() == Some("SomeUnmodeledError") => {}
err @ _ => return Err(err.into()),
},
}
```
</details>
"""
references = ["smithy-rs#76", "smithy-rs#2129", "smithy-rs#2075"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"

[[smithy-rs]]
message = """
The `*Error` and `*ErrorKind` types have been combined to make error matching simpler.
<details>
<summary>Example with S3</summary>
**Before:**
```rust
let result = client
.get_object()
.bucket(BUCKET_NAME)
.key("some-key")
.send()
.await;
match result {
Ok(_output) => { /* Do something with the output */ }
Err(err) => match err.into_service_error() {
GetObjectError { kind, .. } => match kind {
GetObjectErrorKind::InvalidObjectState(value) => println!("invalid object state: {:?}", value),
GetObjectErrorKind::NoSuchKey(_) => println!("object didn't exist"),
}
err @ GetObjectError { .. } if err.code() == Some("SomeUnmodeledError") => {}
err @ _ => return Err(err.into()),
},
}
```
**After:**
```rust
// Needed to access the `.code()` function on the error type:
use aws_sdk_s3::types::ProvideErrorMetadata;
let result = client
.get_object()
.bucket(BUCKET_NAME)
.key("some-key")
.send()
.await;
match result {
Ok(_output) => { /* Do something with the output */ }
Err(err) => match err.into_service_error() {
GetObjectError::InvalidObjectState(value) => {
println!("invalid object state: {:?}", value);
}
GetObjectError::NoSuchKey(_) => {
println!("object didn't exist");
}
err if err.code() == Some("SomeUnmodeledError") => {}
err @ _ => return Err(err.into()),
},
}
```
</details>
"""
references = ["smithy-rs#76", "smithy-rs#2129", "smithy-rs#2075"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client"}
author = "jdisanti"

[[smithy-rs]]
message = "`aws_smithy_types::Error` has been renamed to `aws_smithy_types::error::ErrorMetadata`."
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client"}
author = "jdisanti"

[[aws-sdk-rust]]
message = "`aws_smithy_types::Error` has been renamed to `aws_smithy_types::error::ErrorMetadata`."
references = ["smithy-rs#76", "smithy-rs#2129"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
author = "jdisanti"
8 changes: 4 additions & 4 deletions aws/rust-runtime/aws-config/src/sts/assume_role.rs
Expand Up @@ -7,7 +7,7 @@

use aws_credential_types::cache::CredentialsCache;
use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
use aws_sdk_sts::error::AssumeRoleErrorKind;
use aws_sdk_sts::error::AssumeRoleError;
use aws_sdk_sts::middleware::DefaultMiddleware;
use aws_sdk_sts::model::PolicyDescriptorType;
use aws_sdk_sts::operation::AssumeRole;
Expand Down Expand Up @@ -266,9 +266,9 @@ impl Inner {
}
Err(SdkError::ServiceError(ref context))
if matches!(
context.err().kind,
AssumeRoleErrorKind::RegionDisabledException(_)
| AssumeRoleErrorKind::MalformedPolicyDocumentException(_)
context.err(),
AssumeRoleError::RegionDisabledException(_)
| AssumeRoleError::MalformedPolicyDocumentException(_)
) =>
{
Err(CredentialsError::invalid_configuration(
Expand Down
5 changes: 4 additions & 1 deletion aws/rust-runtime/aws-http/src/lib.rs
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

//! Provides user agent and credentials middleware for the AWS SDK.
//! AWS-specific middleware implementations and HTTP-related features.

#![allow(clippy::derive_partial_eq_without_eq)]
#![warn(
Expand All @@ -28,3 +28,6 @@ pub mod user_agent;

/// AWS-specific content-encoding tools
pub mod content_encoding;

/// AWS-specific request ID support
pub mod request_id;
182 changes: 182 additions & 0 deletions aws/rust-runtime/aws-http/src/request_id.rs
@@ -0,0 +1,182 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

use aws_smithy_http::http::HttpHeaders;
use aws_smithy_http::operation;
use aws_smithy_http::result::SdkError;
use aws_smithy_types::error::metadata::{
Builder as ErrorMetadataBuilder, ErrorMetadata, ProvideErrorMetadata,
};
use aws_smithy_types::error::Unhandled;
use http::{HeaderMap, HeaderValue};

/// Constant for the [`ErrorMetadata`] extra field that contains the request ID
const AWS_REQUEST_ID: &str = "aws_request_id";

/// Implementers add a function to return an AWS request ID
pub trait RequestId {
/// Returns the request ID, or `None` if the service could not be reached.
fn request_id(&self) -> Option<&str>;
}

impl<E, R> RequestId for SdkError<E, R>
where
R: HttpHeaders,
{
fn request_id(&self) -> Option<&str> {
match self {
Self::ResponseError(err) => extract_request_id(err.raw().http_headers()),
Self::ServiceError(err) => extract_request_id(err.raw().http_headers()),
_ => None,
}
}
}

impl RequestId for ErrorMetadata {
fn request_id(&self) -> Option<&str> {
self.extra(AWS_REQUEST_ID)
}
}

impl RequestId for Unhandled {
fn request_id(&self) -> Option<&str> {
self.meta().request_id()
}
}

impl RequestId for operation::Response {
fn request_id(&self) -> Option<&str> {
extract_request_id(self.http().headers())
}
}

impl<B> RequestId for http::Response<B> {
fn request_id(&self) -> Option<&str> {
extract_request_id(self.headers())
}
}

impl<O, E> RequestId for Result<O, E>
where
O: RequestId,
E: RequestId,
{
fn request_id(&self) -> Option<&str> {
match self {
Ok(ok) => ok.request_id(),
Err(err) => err.request_id(),
}
}
}

/// Applies a request ID to a generic error builder
#[doc(hidden)]
pub fn apply_request_id(
builder: ErrorMetadataBuilder,
headers: &HeaderMap<HeaderValue>,
) -> ErrorMetadataBuilder {
if let Some(request_id) = extract_request_id(headers) {
builder.custom(AWS_REQUEST_ID, request_id)
} else {
builder
}
}

/// Extracts a request ID from HTTP response headers
fn extract_request_id(headers: &HeaderMap<HeaderValue>) -> Option<&str> {
headers
.get("x-amzn-requestid")
.or_else(|| headers.get("x-amz-request-id"))
.and_then(|value| value.to_str().ok())
}

#[cfg(test)]
mod tests {
use super::*;
use aws_smithy_http::body::SdkBody;
use http::Response;

#[test]
fn test_request_id_sdk_error() {
let without_request_id =
|| operation::Response::new(Response::builder().body(SdkBody::empty()).unwrap());
let with_request_id = || {
operation::Response::new(
Response::builder()
.header(
"x-amzn-requestid",
HeaderValue::from_static("some-request-id"),
)
.body(SdkBody::empty())
.unwrap(),
)
};
assert_eq!(
None,
SdkError::<(), _>::response_error("test", without_request_id()).request_id()
);
assert_eq!(
Some("some-request-id"),
SdkError::<(), _>::response_error("test", with_request_id()).request_id()
);
assert_eq!(
None,
SdkError::service_error((), without_request_id()).request_id()
);
assert_eq!(
Some("some-request-id"),
SdkError::service_error((), with_request_id()).request_id()
);
}

#[test]
fn test_extract_request_id() {
let mut headers = HeaderMap::new();
assert_eq!(None, extract_request_id(&headers));

headers.append(
"x-amzn-requestid",
HeaderValue::from_static("some-request-id"),
);
assert_eq!(Some("some-request-id"), extract_request_id(&headers));

headers.append(
"x-amz-request-id",
HeaderValue::from_static("other-request-id"),
);
assert_eq!(Some("some-request-id"), extract_request_id(&headers));

headers.remove("x-amzn-requestid");
assert_eq!(Some("other-request-id"), extract_request_id(&headers));
}

#[test]
fn test_apply_request_id() {
let mut headers = HeaderMap::new();
assert_eq!(
ErrorMetadata::builder().build(),
apply_request_id(ErrorMetadata::builder(), &headers).build(),
);

headers.append(
"x-amzn-requestid",
HeaderValue::from_static("some-request-id"),
);
assert_eq!(
ErrorMetadata::builder()
.custom(AWS_REQUEST_ID, "some-request-id")
.build(),
apply_request_id(ErrorMetadata::builder(), &headers).build(),
);
}

#[test]
fn test_error_metadata_request_id_impl() {
let err = ErrorMetadata::builder()
.custom(AWS_REQUEST_ID, "some-request-id")
.build();
assert_eq!(Some("some-request-id"), err.request_id());
}
}
4 changes: 2 additions & 2 deletions aws/rust-runtime/aws-inlineable/src/lib.rs
Expand Up @@ -25,8 +25,8 @@ pub mod no_credentials;
/// Support types required for adding presigning to an operation in a generated service.
pub mod presigning;

/// Special logic for handling S3's error responses.
pub mod s3_errors;
/// Special logic for extracting request IDs from S3's responses.
pub mod s3_request_id;

/// Glacier-specific checksumming behavior
pub mod glacier_checksums;
Expand Down

0 comments on commit d48878e

Please sign in to comment.