Unverified Commit d48878ee authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

Implement request ID access for SDK clients RFC (#2129)

* 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`
parent 0a11d519
Loading
Loading
Loading
Loading
+164 −0
Original line number Diff line number Diff line
@@ -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"
+4 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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(
+4 −1
Original line number Diff line number Diff line
@@ -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(
@@ -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 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -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;
Loading