awslabs / aws-lambda-rust-runtime

A Rust runtime for AWS Lambda
Apache License 2.0
3.3k stars 335 forks source link

Allow error response customization #828

Closed kikuomax closed 6 months ago

kikuomax commented 6 months ago

Issue #, if available:

Description of changes:

By submitting this pull request

kikuomax commented 6 months ago

@evbo Have you tried something like the following?

#[derive(thiserror::Error, Debug)]
pub enum MyError {
    #[error("some error: {0}")]
    SomeError(String),
    // ... other variants
}

impl<'a> Into<Diagnostic<'a>> for MyError {
    fn into(self) -> Diagnostic<'a> {
        match self {
            Self::SomeError(s) => Diagnostic {
                error_type: Cow::Borrowed("SomeError"),
                error_message: Cow::Owned(s),
            },
            // ... other variants
        }
    }
}

The above will cause conflict between From<Display> for Diagnostic and Into<Diagnostic> for MyError, because MyError implements derived Display. So you cannot directly implement Diagnostic for MyError; more generally, any type implementing std::error::Error.

A workaround in this case may be to define a wrapper struct for your error type and implement Diagnostic for the wrapper like in the example of Diagnostic:

struct ErrorResponse(MyError);

impl<'a> Into<Diagnostic<'a>> for ErrorResponse {
    fn into(self) -> Diagnostic<'a> {
        match self.0 {
            ErrorResponse::SomeError(s) => Diagnostic {
                error_type: Cow::Borrowed("SomeError"),
                error_message: Cow::Owned(s),
            },
            // ... other variants
        }
    }
}

Display must not be implemented for ErrorResponse to avoid the same conflict.

Obviously, I should refine the documentation.

evbo commented 6 months ago

@kikuomax I take back my earlier messages. I think you're right - this can be solved with better docs.

I think the best ergonomics are achieved when function_handler returns the error and then you map_err to the top-level struct by intercepting what gets returned to lambda_runtime::run as shown here: https://docs.aws.amazon.com/lambda/latest/dg/rust-handler.html#rust-shared-state

kikuomax commented 6 months ago

@evbo I am planning a PR to improve the documentation. How about having the following in the API documentation? Does it make better sense?

Diagnostic is automatically generated from any types that implement Display; e.g., Error. The default behavior puts the type name of the original error obtained with std::any::type_name into error_type, which may not be reliable for conditional error handling. You can define your own error container that implements Into<Diagnostic> if you need more accurate error types.

Example:

use lambda_runtime::{service_fn, Diagnostic, Error, LambdaEvent};

#[derive(Debug)]
struct ErrorResponse(Error);

impl<'a> Into<Diagnostic<'a>> for ErrorResponse {
    fn into(self) -> Diagnostic<'a> {
        Diagnostic {
            error_type: "MyError".into(),
            error_message: self.0.to_string().into(),
        }
    }
}

async fn function_handler(_event: LambdaEvent<()>) -> Result<(), Error> {
   // ... do something
   Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    lambda_runtime::run(service_fn(|event| async {
        function_handler(event).await.map_err(ErrorResponse)
    })).await
}

The container ErrorResponse in the above snippet is necessary because you cannot directly implement Into<Diagnostic> for types that implement Error. For instance, the following code will not compile:

ⓘ compile_fail

use lambda_runtime::Diagnostic;

#[derive(Debug)]
struct MyError(String);

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyError: {}", self.0)
    }
}

impl std::error::Error for MyError {}

// ERROR! conflicting implementations of trait `Into<Diagnostic<'_>>` for type `MyError`
impl<'a> Into<Diagnostic<'a>> for MyError {
    fn into(self) -> Diagnostic<'a> {
        Diagnostic {
            error_type: "MyError".into(),
            error_message: self.0.into(),
        }
    }
}