Skip to content

Commit 150b3d4

Browse files
committed
Relax blanket implementation of Diagnostic
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]>
1 parent 92cdd74 commit 150b3d4

File tree

17 files changed

+233
-110
lines changed

17 files changed

+233
-110
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ exclude = ["examples"]
1414
[workspace.dependencies]
1515
base64 = "0.22"
1616
bytes = "1"
17-
chrono = "0.4.35"
17+
chrono = { version = "0.4.35", default-features = false, features = [
18+
"clock",
19+
"serde",
20+
"std",
21+
] }
1822
futures = "0.3"
1923
futures-channel = "0.3"
2024
futures-util = "0.3"

examples/advanced-sqs-multiple-functions-shared-data/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[workspace]
2+
resolver = "2"
23

34
members = [
45
"producer",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "basic-error-diagnostic"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# Starting in Rust 1.62 you can use `cargo add` to add dependencies
7+
# to your project.
8+
#
9+
# If you're using an older Rust version,
10+
# download cargo-edit(https://github.com/killercup/cargo-edit#installation)
11+
# to install the `add` subcommand.
12+
#
13+
# Running `cargo add DEPENDENCY_NAME` will
14+
# add the latest version of a dependency to the list,
15+
# and it will keep the alphabetic ordering for you.
16+
17+
[dependencies]
18+
19+
lambda_runtime = { path = "../../lambda-runtime" }
20+
serde = "1"
21+
thiserror = "1.0.61"
22+
tokio = { version = "1", features = ["macros"] }
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# AWS Lambda Function Error handling example
2+
3+
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.
4+
5+
## Build & Deploy
6+
7+
1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation)
8+
2. Build the function with `cargo lambda build --release`
9+
3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE`
10+
11+
## Build for ARM 64
12+
13+
Build the function with `cargo lambda build --release --arm64`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use lambda_runtime::{service_fn, Diagnostic, Error, LambdaEvent};
2+
use serde::Deserialize;
3+
use thiserror;
4+
5+
#[derive(Deserialize)]
6+
struct Request {}
7+
8+
#[derive(Debug, thiserror::Error)]
9+
pub enum ExecutionError {
10+
#[error("transient database error: {0}")]
11+
DatabaseError(String),
12+
#[error("unexpected error: {0}")]
13+
Unexpected(String),
14+
}
15+
16+
impl<'a> From<ExecutionError> for Diagnostic<'a> {
17+
fn from(value: ExecutionError) -> Diagnostic<'a> {
18+
let (error_type, error_message) = match value {
19+
ExecutionError::DatabaseError(err) => ("Retryable", err.to_string()),
20+
ExecutionError::Unexpected(err) => ("NonRetryable", err.to_string()),
21+
};
22+
Diagnostic {
23+
error_type: error_type.into(),
24+
error_message: error_message.into(),
25+
}
26+
}
27+
}
28+
29+
/// This is the main body for the Lambda function
30+
async fn function_handler(_event: LambdaEvent<Request>) -> Result<(), ExecutionError> {
31+
Err(ExecutionError::Unexpected("ooops".to_string()))
32+
}
33+
34+
#[tokio::main]
35+
async fn main() -> Result<(), Error> {
36+
lambda_runtime::run(service_fn(function_handler)).await
37+
}

examples/basic-error-handling/README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# AWS Lambda Function example
1+
# AWS Lambda Function Error handling example
2+
3+
This example shows how to return a custom error type for unexpected failures.
24

35
## Build & Deploy
46

examples/basic-error-handling/src/main.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// See https://github.com/awslabs/aws-lambda-rust-runtime for more info on Rust runtime for AWS Lambda
22
use lambda_runtime::{service_fn, tracing, Error, LambdaEvent};
33
use serde::{Deserialize, Serialize};
4-
use serde_json::{json, Value};
4+
use serde_json::json;
55
use std::fs::File;
66

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

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

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

97-
return Ok(json!(resp));
97+
return Ok(resp);
9898
}
9999
}
100100
}

lambda-events/Cargo.toml

+1-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,7 @@ edition = "2021"
1818
[dependencies]
1919
base64 = { workspace = true }
2020
bytes = { workspace = true, features = ["serde"], optional = true }
21-
chrono = { workspace = true, default-features = false, features = [
22-
"clock",
23-
"serde",
24-
"std",
25-
], optional = true }
21+
chrono = { workspace = true, optional = true }
2622
flate2 = { version = "1.0.24", optional = true }
2723
http = { workspace = true, optional = true }
2824
http-body = { workspace = true, optional = true }

lambda-http/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub use http::{self, Response};
6868
/// Utilities to initialize and use `tracing` and `tracing-subscriber` in Lambda Functions.
6969
#[cfg(feature = "tracing")]
7070
pub use lambda_runtime::tracing;
71+
use lambda_runtime::Diagnostic;
7172
pub use lambda_runtime::{self, service_fn, tower, Context, Error, LambdaEvent, Service};
7273
use request::RequestFuture;
7374
use response::ResponseFuture;
@@ -193,7 +194,7 @@ where
193194
S: Service<Request, Response = R, Error = E>,
194195
S::Future: Send + 'a,
195196
R: IntoResponse,
196-
E: std::fmt::Debug + std::fmt::Display,
197+
E: std::fmt::Debug + for<'b> Into<Diagnostic<'b>>,
197198
{
198199
lambda_runtime::run(Adapter::from(handler)).await
199200
}

lambda-http/src/streaming.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ use crate::{request::LambdaRequest, RequestExt};
55
use bytes::Bytes;
66
pub use http::{self, Response};
77
use http_body::Body;
8+
use lambda_runtime::Diagnostic;
89
pub use lambda_runtime::{self, tower::ServiceExt, Error, LambdaEvent, MetadataPrelude, Service, StreamResponse};
9-
use std::fmt::{Debug, Display};
10+
use std::fmt::Debug;
1011
use std::pin::Pin;
1112
use std::task::{Context, Poll};
1213
use tokio_stream::Stream;
@@ -20,7 +21,7 @@ pub async fn run_with_streaming_response<'a, S, B, E>(handler: S) -> Result<(),
2021
where
2122
S: Service<Request, Response = Response<B>, Error = E>,
2223
S::Future: Send + 'a,
23-
E: Debug + Display,
24+
E: Debug + for<'b> Into<Diagnostic<'b>>,
2425
B: Body + Unpin + Send + 'static,
2526
B::Data: Into<Bytes> + Send,
2627
B::Error: Into<Error> + Send + Debug,

lambda-runtime/src/diagnostic.rs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use serde::{Deserialize, Serialize};
2+
use std::borrow::Cow;
3+
4+
use crate::{deserializer::DeserializeError, Error};
5+
6+
/// Diagnostic information about an error.
7+
///
8+
/// `Diagnostic` is automatically derived for some common types,
9+
/// like boxed types that implement [`Error`][std::error::Error].
10+
///
11+
/// [`error_type`][`Diagnostic::error_type`] is derived from the type name of
12+
/// the original error with [`std::any::type_name`] as a fallback, which may
13+
/// not be reliable for conditional error handling.
14+
/// You can define your own error container that implements `Into<Diagnostic>`
15+
/// if you need to handle errors based on error types.
16+
///
17+
/// Example:
18+
/// ```
19+
/// use lambda_runtime::{Diagnostic, Error, LambdaEvent};
20+
/// use std::borrow::Cow;
21+
///
22+
/// #[derive(Debug)]
23+
/// struct ErrorResponse(Error);
24+
///
25+
/// impl<'a> Into<Diagnostic<'a>> for ErrorResponse {
26+
/// fn into(self) -> Diagnostic<'a> {
27+
/// Diagnostic {
28+
/// error_type: "MyError".into(),
29+
/// error_message: self.0.to_string().into(),
30+
/// }
31+
/// }
32+
/// }
33+
///
34+
/// async fn function_handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> {
35+
/// // ... do something
36+
/// Ok(())
37+
/// }
38+
/// ```
39+
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
40+
#[serde(rename_all = "camelCase")]
41+
pub struct Diagnostic<'a> {
42+
/// Error type.
43+
///
44+
/// `error_type` is derived from the type name of the original error with
45+
/// [`std::any::type_name`] as a fallback.
46+
/// Please implement your own `Into<Diagnostic>` if you need more reliable
47+
/// error types.
48+
pub error_type: Cow<'a, str>,
49+
/// Error message.
50+
///
51+
/// `error_message` is the output from the [`Display`][std::fmt::Display]
52+
/// implementation of the original error as a fallback.
53+
pub error_message: Cow<'a, str>,
54+
}
55+
56+
impl<'a> From<DeserializeError> for Diagnostic<'a> {
57+
fn from(value: DeserializeError) -> Self {
58+
Diagnostic {
59+
error_type: std::any::type_name::<DeserializeError>().into(),
60+
error_message: value.to_string().into(),
61+
}
62+
}
63+
}
64+
65+
impl<'a> From<Error> for Diagnostic<'a> {
66+
fn from(value: Error) -> Self {
67+
Diagnostic {
68+
error_type: std::any::type_name::<Error>().into(),
69+
error_message: value.to_string().into(),
70+
}
71+
}
72+
}
73+
74+
impl<'a, T> From<Box<T>> for Diagnostic<'a>
75+
where
76+
T: std::error::Error,
77+
{
78+
fn from(value: Box<T>) -> Self {
79+
Diagnostic {
80+
error_type: std::any::type_name::<T>().into(),
81+
error_message: value.to_string().into(),
82+
}
83+
}
84+
}
85+
86+
impl<'a> From<Box<dyn std::error::Error>> for Diagnostic<'a> {
87+
fn from(value: Box<dyn std::error::Error>) -> Self {
88+
Diagnostic {
89+
error_type: std::any::type_name::<Box<dyn std::error::Error>>().into(),
90+
error_message: value.to_string().into(),
91+
}
92+
}
93+
}
94+
95+
impl<'a> From<std::convert::Infallible> for Diagnostic<'a> {
96+
fn from(value: std::convert::Infallible) -> Self {
97+
Diagnostic {
98+
error_type: std::any::type_name::<std::convert::Infallible>().into(),
99+
error_message: value.to_string().into(),
100+
}
101+
}
102+
}
103+
104+
#[cfg(test)]
105+
mod test {
106+
use super::*;
107+
108+
#[test]
109+
fn round_trip_lambda_error() {
110+
use serde_json::{json, Value};
111+
let expected = json!({
112+
"errorType": "InvalidEventDataError",
113+
"errorMessage": "Error parsing event data.",
114+
});
115+
116+
let actual = Diagnostic {
117+
error_type: "InvalidEventDataError".into(),
118+
error_message: "Error parsing event data.".into(),
119+
};
120+
let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic");
121+
assert_eq!(expected, actual);
122+
}
123+
}

lambda-runtime/src/layers/api_response.rs

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use crate::requests::{EventCompletionRequest, IntoRequest};
2-
use crate::runtime::LambdaInvocation;
3-
use crate::types::Diagnostic;
4-
use crate::{deserializer, IntoFunctionResponse};
5-
use crate::{EventErrorRequest, LambdaEvent};
1+
use crate::{
2+
deserializer,
3+
requests::{EventCompletionRequest, IntoRequest},
4+
runtime::LambdaInvocation,
5+
Diagnostic, EventErrorRequest, IntoFunctionResponse, LambdaEvent,
6+
};
67
use futures::ready;
78
use futures::Stream;
89
use lambda_runtime_api_client::{body::Body, BoxError};

lambda-runtime/src/lib.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ use tokio_stream::Stream;
1818
use tower::util::ServiceFn;
1919
pub use tower::{self, service_fn, Service};
2020

21+
/// Diagnostic utilities to convert Rust types into Lambda Error types.
22+
pub mod diagnostic;
23+
pub use diagnostic::Diagnostic;
24+
2125
mod deserializer;
22-
/// Tower middleware to be applied to runtime invocatinos.
26+
/// Tower middleware to be applied to runtime invocations.
2327
pub mod layers;
2428
mod requests;
2529
mod runtime;
@@ -35,9 +39,7 @@ mod types;
3539

3640
use requests::EventErrorRequest;
3741
pub use runtime::{LambdaInvocation, Runtime};
38-
pub use types::{
39-
Context, Diagnostic, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse,
40-
};
42+
pub use types::{Context, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse};
4143

4244
/// Error type that lambdas may result in
4345
pub type Error = lambda_runtime_api_client::BoxError;

lambda-runtime/src/requests.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::types::ToStreamErrorTrailer;
2-
use crate::{types::Diagnostic, Error, FunctionResponse, IntoFunctionResponse};
1+
use crate::{types::ToStreamErrorTrailer, Diagnostic, Error, FunctionResponse, IntoFunctionResponse};
32
use bytes::Bytes;
43
use http::header::CONTENT_TYPE;
54
use http::{Method, Request, Uri};

lambda-runtime/src/runtime.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use super::requests::{IntoRequest, NextEventRequest};
2-
use super::types::{invoke_request_id, Diagnostic, IntoFunctionResponse, LambdaEvent};
31
use crate::layers::{CatchPanicService, RuntimeApiClientService, RuntimeApiResponseService};
4-
use crate::{Config, Context};
2+
use crate::requests::{IntoRequest, NextEventRequest};
3+
use crate::types::{invoke_request_id, IntoFunctionResponse, LambdaEvent};
4+
use crate::{Config, Context, Diagnostic};
55
use http_body_util::BodyExt;
66
use lambda_runtime_api_client::BoxError;
77
use lambda_runtime_api_client::Client as ApiClient;
@@ -252,8 +252,7 @@ mod endpoint_tests {
252252
use super::{incoming, wrap_handler};
253253
use crate::{
254254
requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest},
255-
types::Diagnostic,
256-
Config, Error, Runtime,
255+
Config, Diagnostic, Error, Runtime,
257256
};
258257
use futures::future::BoxFuture;
259258
use http::{HeaderValue, StatusCode};

0 commit comments

Comments
 (0)