Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax blanket implementation of Diagnostic #897

Merged
merged 1 commit into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading