Skip to content

Commit

Permalink
Relax blanket implementation of Diagnostic (#897)
Browse files Browse the repository at this point in the history
Instead of implementing Diagnostic for everything that implements Display, implement the trait only for a few well known types. This gives people more flexibility to implement Diagnostic.

Signed-off-by: David Calavera <[email protected]>
  • Loading branch information
calavera authored Jun 25, 2024
1 parent 92cdd74 commit cc239ea
Show file tree
Hide file tree
Showing 17 changed files with 251 additions and 110 deletions.
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ exclude = ["examples"]
[workspace.dependencies]
base64 = "0.22"
bytes = "1"
chrono = "0.4.35"
chrono = { version = "0.4.35", default-features = false, features = [
"clock",
"serde",
"std",
] }
futures = "0.3"
futures-channel = "0.3"
futures-util = "0.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[workspace]
resolver = "2"

members = [
"producer",
Expand Down
1 change: 1 addition & 0 deletions examples/basic-error-diagnostic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
22 changes: 22 additions & 0 deletions examples/basic-error-diagnostic/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "basic-error-diagnostic"
version = "0.1.0"
edition = "2021"

# Starting in Rust 1.62 you can use `cargo add` to add dependencies
# to your project.
#
# If you're using an older Rust version,
# download cargo-edit(https://github.com/killercup/cargo-edit#installation)
# to install the `add` subcommand.
#
# Running `cargo add DEPENDENCY_NAME` will
# add the latest version of a dependency to the list,
# and it will keep the alphabetic ordering for you.

[dependencies]

lambda_runtime = { path = "../../lambda-runtime" }
serde = "1"
thiserror = "1.0.61"
tokio = { version = "1", features = ["macros"] }
13 changes: 13 additions & 0 deletions examples/basic-error-diagnostic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# AWS Lambda Function Error handling example

This example shows how to implement the `Diagnostic` trait to return a specific `error_type` in the Lambda error response. If you don't use the `error_type` field, you don't need to implement `Diagnostic`, the type will be generated based on the error type name.

## Build & Deploy

1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation)
2. Build the function with `cargo lambda build --release`
3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE`

## Build for ARM 64

Build the function with `cargo lambda build --release --arm64`
37 changes: 37 additions & 0 deletions examples/basic-error-diagnostic/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use lambda_runtime::{service_fn, Diagnostic, Error, LambdaEvent};
use serde::Deserialize;
use thiserror;

#[derive(Deserialize)]
struct Request {}

#[derive(Debug, thiserror::Error)]
pub enum ExecutionError {
#[error("transient database error: {0}")]
DatabaseError(String),
#[error("unexpected error: {0}")]
Unexpected(String),
}

impl<'a> From<ExecutionError> for Diagnostic<'a> {
fn from(value: ExecutionError) -> Diagnostic<'a> {
let (error_type, error_message) = match value {
ExecutionError::DatabaseError(err) => ("Retryable", err.to_string()),
ExecutionError::Unexpected(err) => ("NonRetryable", err.to_string()),
};
Diagnostic {
error_type: error_type.into(),
error_message: error_message.into(),
}
}
}

/// This is the main body for the Lambda function
async fn function_handler(_event: LambdaEvent<Request>) -> Result<(), ExecutionError> {
Err(ExecutionError::Unexpected("ooops".to_string()))
}

#[tokio::main]
async fn main() -> Result<(), Error> {
lambda_runtime::run(service_fn(function_handler)).await
}
4 changes: 3 additions & 1 deletion examples/basic-error-handling/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# AWS Lambda Function example
# AWS Lambda Function Error handling example

This example shows how to return a custom error type for unexpected failures.

## Build & Deploy

Expand Down
8 changes: 4 additions & 4 deletions examples/basic-error-handling/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// See https://github.com/awslabs/aws-lambda-rust-runtime for more info on Rust runtime for AWS Lambda
use lambda_runtime::{service_fn, tracing, Error, LambdaEvent};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::json;
use std::fs::File;

/// A simple Lambda request structure with just one field
Expand Down Expand Up @@ -59,11 +59,11 @@ async fn main() -> Result<(), Error> {
}

/// The actual handler of the Lambda request.
pub(crate) async fn func(event: LambdaEvent<Value>) -> Result<Value, Error> {
pub(crate) async fn func(event: LambdaEvent<Request>) -> Result<Response, Error> {
let (event, ctx) = event.into_parts();

// check what action was requested
match serde_json::from_value::<Request>(event)?.event_type {
match event.event_type {
EventType::SimpleError => {
// generate a simple text message error using `simple_error` crate
return Err(Box::new(simple_error::SimpleError::new("A simple error as requested!")));
Expand Down Expand Up @@ -94,7 +94,7 @@ pub(crate) async fn func(event: LambdaEvent<Value>) -> Result<Value, Error> {
msg: "OK".into(),
};

return Ok(json!(resp));
return Ok(resp);
}
}
}
6 changes: 1 addition & 5 deletions lambda-events/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ edition = "2021"
[dependencies]
base64 = { workspace = true }
bytes = { workspace = true, features = ["serde"], optional = true }
chrono = { workspace = true, default-features = false, features = [
"clock",
"serde",
"std",
], optional = true }
chrono = { workspace = true, optional = true }
flate2 = { version = "1.0.24", optional = true }
http = { workspace = true, optional = true }
http-body = { workspace = true, optional = true }
Expand Down
3 changes: 2 additions & 1 deletion lambda-http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub use http::{self, Response};
/// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions.
#[cfg(feature = "tracing")]
pub use lambda_runtime::tracing;
use lambda_runtime::Diagnostic;
pub use lambda_runtime::{self, service_fn, tower, Context, Error, LambdaEvent, Service};
use request::RequestFuture;
use response::ResponseFuture;
Expand Down Expand Up @@ -193,7 +194,7 @@ where
S: Service<Request, Response = R, Error = E>,
S::Future: Send + 'a,
R: IntoResponse,
E: std::fmt::Debug + std::fmt::Display,
E: std::fmt::Debug + for<'b> Into<Diagnostic<'b>>,
{
lambda_runtime::run(Adapter::from(handler)).await
}
Expand Down
5 changes: 3 additions & 2 deletions lambda-http/src/streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use crate::{request::LambdaRequest, RequestExt};
use bytes::Bytes;
pub use http::{self, Response};
use http_body::Body;
use lambda_runtime::Diagnostic;
pub use lambda_runtime::{self, tower::ServiceExt, Error, LambdaEvent, MetadataPrelude, Service, StreamResponse};
use std::fmt::{Debug, Display};
use std::fmt::Debug;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio_stream::Stream;
Expand All @@ -20,7 +21,7 @@ pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(),
where
S: Service<Request, Response = Response<B>, Error = E>,
S::Future: Send + 'a,
E: Debug + Display,
E: Debug + for<'b> Into<Diagnostic<'b>>,
B: Body + Unpin + Send + 'static,
B::Data: Into<Bytes> + Send,
B::Error: Into<Error> + Send + Debug,
Expand Down
141 changes: 141 additions & 0 deletions lambda-runtime/src/diagnostic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use serde::{Deserialize, Serialize};
use std::{any::type_name, borrow::Cow};

use crate::{deserializer::DeserializeError, Error};

/// Diagnostic information about an error.
///
/// `Diagnostic` is automatically derived for some common types,
/// like boxed types that implement [`Error`][std::error::Error].
///
/// [`error_type`][`Diagnostic::error_type`] is derived from the type name of
/// the original error with [`std::any::type_name`] as a fallback, which may
/// not be reliable for conditional error handling.
/// You can define your own error container that implements `Into<Diagnostic>`
/// if you need to handle errors based on error types.
///
/// Example:
/// ```
/// use lambda_runtime::{Diagnostic, Error, LambdaEvent};
/// use std::borrow::Cow;
///
/// #[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<(), ErrorResponse> {
/// // ... do something
/// Ok(())
/// }
/// ```
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Diagnostic<'a> {
/// Error type.
///
/// `error_type` is derived from the type name of the original error with
/// [`std::any::type_name`] as a fallback.
/// Please implement your own `Into<Diagnostic>` if you need more reliable
/// error types.
pub error_type: Cow<'a, str>,
/// Error message.
///
/// `error_message` is the output from the [`Display`][std::fmt::Display]
/// implementation of the original error as a fallback.
pub error_message: Cow<'a, str>,
}

impl<'a> From<DeserializeError> for Diagnostic<'a> {
fn from(value: DeserializeError) -> Self {
Diagnostic {
error_type: type_name::<DeserializeError>().into(),
error_message: value.to_string().into(),
}
}
}

impl<'a> From<Error> for Diagnostic<'a> {
fn from(value: Error) -> Self {
Diagnostic {
error_type: type_name::<Error>().into(),
error_message: value.to_string().into(),
}
}
}

impl<'a, T> From<Box<T>> for Diagnostic<'a>
where
T: std::error::Error,
{
fn from(value: Box<T>) -> Self {
Diagnostic {
error_type: type_name::<T>().into(),
error_message: value.to_string().into(),
}
}
}

impl<'a> From<Box<dyn std::error::Error>> for Diagnostic<'a> {
fn from(value: Box<dyn std::error::Error>) -> Self {
Diagnostic {
error_type: type_name::<Box<dyn std::error::Error>>().into(),
error_message: value.to_string().into(),
}
}
}

impl<'a> From<std::convert::Infallible> for Diagnostic<'a> {
fn from(value: std::convert::Infallible) -> Self {
Diagnostic {
error_type: type_name::<std::convert::Infallible>().into(),
error_message: value.to_string().into(),
}
}
}

impl<'a> From<String> for Diagnostic<'a> {
fn from(value: String) -> Self {
Diagnostic {
error_type: type_name::<String>().into(),
error_message: value.into(),
}
}
}

impl<'a> From<&'static str> for Diagnostic<'a> {
fn from(value: &'static str) -> Self {
Diagnostic {
error_type: type_name::<&'static str>().into(),
error_message: value.into(),
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn round_trip_lambda_error() {
use serde_json::{json, Value};
let expected = json!({
"errorType": "InvalidEventDataError",
"errorMessage": "Error parsing event data.",
});

let actual = Diagnostic {
error_type: "InvalidEventDataError".into(),
error_message: "Error parsing event data.".into(),
};
let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic");
assert_eq!(expected, actual);
}
}
11 changes: 6 additions & 5 deletions lambda-runtime/src/layers/api_response.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::requests::{EventCompletionRequest, IntoRequest};
use crate::runtime::LambdaInvocation;
use crate::types::Diagnostic;
use crate::{deserializer, IntoFunctionResponse};
use crate::{EventErrorRequest, LambdaEvent};
use crate::{
deserializer,
requests::{EventCompletionRequest, IntoRequest},
runtime::LambdaInvocation,
Diagnostic, EventErrorRequest, IntoFunctionResponse, LambdaEvent,
};
use futures::ready;
use futures::Stream;
use lambda_runtime_api_client::{body::Body, BoxError};
Expand Down
10 changes: 6 additions & 4 deletions lambda-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ use tokio_stream::Stream;
use tower::util::ServiceFn;
pub use tower::{self, service_fn, Service};

/// Diagnostic utilities to convert Rust types into Lambda Error types.
pub mod diagnostic;
pub use diagnostic::Diagnostic;

mod deserializer;
/// Tower middleware to be applied to runtime invocatinos.
/// Tower middleware to be applied to runtime invocations.
pub mod layers;
mod requests;
mod runtime;
Expand All @@ -35,9 +39,7 @@ mod types;

use requests::EventErrorRequest;
pub use runtime::{LambdaInvocation, Runtime};
pub use types::{
Context, Diagnostic, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse,
};
pub use types::{Context, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse};

/// Error type that lambdas may result in
pub type Error = lambda_runtime_api_client::BoxError;
Expand Down
3 changes: 1 addition & 2 deletions lambda-runtime/src/requests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::types::ToStreamErrorTrailer;
use crate::{types::Diagnostic, Error, FunctionResponse, IntoFunctionResponse};
use crate::{types::ToStreamErrorTrailer, Diagnostic, Error, FunctionResponse, IntoFunctionResponse};
use bytes::Bytes;
use http::header::CONTENT_TYPE;
use http::{Method, Request, Uri};
Expand Down
9 changes: 4 additions & 5 deletions lambda-runtime/src/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::requests::{IntoRequest, NextEventRequest};
use super::types::{invoke_request_id, Diagnostic, IntoFunctionResponse, LambdaEvent};
use crate::layers::{CatchPanicService, RuntimeApiClientService, RuntimeApiResponseService};
use crate::{Config, Context};
use crate::requests::{IntoRequest, NextEventRequest};
use crate::types::{invoke_request_id, IntoFunctionResponse, LambdaEvent};
use crate::{Config, Context, Diagnostic};
use http_body_util::BodyExt;
use lambda_runtime_api_client::BoxError;
use lambda_runtime_api_client::Client as ApiClient;
Expand Down Expand Up @@ -252,8 +252,7 @@ mod endpoint_tests {
use super::{incoming, wrap_handler};
use crate::{
requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest},
types::Diagnostic,
Config, Error, Runtime,
Config, Diagnostic, Error, Runtime,
};
use futures::future::BoxFuture;
use http::{HeaderValue, StatusCode};
Expand Down
Loading

0 comments on commit cc239ea

Please sign in to comment.