diff --git a/lambda-runtime/src/lib.rs b/lambda-runtime/src/lib.rs index effb0561..b4964954 100644 --- a/lambda-runtime/src/lib.rs +++ b/lambda-runtime/src/lib.rs @@ -14,8 +14,9 @@ use hyper::{body::Incoming, http::Request}; use lambda_runtime_api_client::{body::Body, BoxError, Client}; use serde::{Deserialize, Serialize}; use std::{ + borrow::Cow, env, - fmt::{self, Debug, Display}, + fmt::{self, Debug}, future::Future, panic, sync::Arc, @@ -33,7 +34,9 @@ pub mod streaming; mod types; use requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest}; -pub use types::{Context, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse}; +pub use types::{ + Context, Diagnostic, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse, +}; use types::invoke_request_id; @@ -96,7 +99,7 @@ impl Runtime { where F: Service>, F::Future: Future>, - F::Error: fmt::Debug + fmt::Display, + F::Error: for<'a> Into> + fmt::Debug, A: for<'de> Deserialize<'de>, R: IntoFunctionResponse, B: Serialize, @@ -173,7 +176,14 @@ impl Runtime { } else { "Lambda panicked".to_string() }; - EventErrorRequest::new(request_id, error_type, &msg).into_req() + EventErrorRequest::new( + request_id, + Diagnostic { + error_type: Cow::Borrowed(error_type), + error_message: Cow::Owned(msg), + }, + ) + .into_req() } } } @@ -224,7 +234,7 @@ pub async fn run(handler: F) -> Result<(), Error> where F: Service>, F::Future: Future>, - F::Error: fmt::Debug + fmt::Display, + F::Error: for<'a> Into> + fmt::Debug, A: for<'de> Deserialize<'de>, R: IntoFunctionResponse, B: Serialize, @@ -249,15 +259,12 @@ fn type_name_of_val(_: T) -> &'static str { std::any::type_name::() } -fn build_event_error_request(request_id: &str, err: T) -> Result, Error> +fn build_event_error_request<'a, T>(request_id: &'a str, err: T) -> Result, Error> where - T: Display + Debug, + T: Into> + Debug, { error!("{:?}", err); // logs the error in CloudWatch - let error_type = type_name_of_val(&err); - let msg = format!("{err}"); - - EventErrorRequest::new(request_id, error_type, &msg).into_req() + EventErrorRequest::new(request_id, err).into_req() } #[cfg(test)] @@ -274,7 +281,7 @@ mod endpoint_tests { use httpmock::prelude::*; use lambda_runtime_api_client::Client; - use std::{env, sync::Arc}; + use std::{borrow::Cow, env, sync::Arc}; use tokio_stream::StreamExt; #[tokio::test] @@ -341,8 +348,8 @@ mod endpoint_tests { #[tokio::test] async fn test_error_response() -> Result<(), Error> { let diagnostic = Diagnostic { - error_type: "InvalidEventDataError", - error_message: "Error parsing event data", + error_type: Cow::Borrowed("InvalidEventDataError"), + error_message: Cow::Borrowed("Error parsing event data"), }; let body = serde_json::to_string(&diagnostic)?; diff --git a/lambda-runtime/src/requests.rs b/lambda-runtime/src/requests.rs index c9274cf4..d1e25e32 100644 --- a/lambda-runtime/src/requests.rs +++ b/lambda-runtime/src/requests.rs @@ -194,13 +194,10 @@ pub(crate) struct EventErrorRequest<'a> { } impl<'a> EventErrorRequest<'a> { - pub(crate) fn new(request_id: &'a str, error_type: &'a str, error_message: &'a str) -> EventErrorRequest<'a> { + pub(crate) fn new(request_id: &'a str, diagnostic: impl Into>) -> EventErrorRequest<'a> { EventErrorRequest { request_id, - diagnostic: Diagnostic { - error_type, - error_message, - }, + diagnostic: diagnostic.into(), } } } @@ -226,8 +223,8 @@ fn test_event_error_request() { let req = EventErrorRequest { request_id: "id", diagnostic: Diagnostic { - error_type: "InvalidEventDataError", - error_message: "Error parsing event data", + error_type: std::borrow::Cow::Borrowed("InvalidEventDataError"), + error_message: std::borrow::Cow::Borrowed("Error parsing event data"), }, }; let req = req.into_req().unwrap(); diff --git a/lambda-runtime/src/types.rs b/lambda-runtime/src/types.rs index 3d89e0a0..478f88fd 100644 --- a/lambda-runtime/src/types.rs +++ b/lambda-runtime/src/types.rs @@ -5,19 +5,75 @@ use http::{header::ToStrError, HeaderMap, HeaderValue, StatusCode}; use lambda_runtime_api_client::body::Body; use serde::{Deserialize, Serialize}; use std::{ + borrow::Cow, collections::HashMap, env, - fmt::Debug, + fmt::{Debug, Display}, time::{Duration, SystemTime}, }; use tokio_stream::Stream; use tracing::Span; +/// Diagnostic information about an error. +/// +/// `Diagnostic` is automatically derived for types that implement +/// [`Display`][std::fmt::Display]; e.g., [`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` +/// 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> for ErrorResponse { +/// fn into(self) -> Diagnostic<'a> { +/// Diagnostic { +/// error_type: Cow::Borrowed("MyError"), +/// error_message: Cow::Owned(self.0.to_string()), +/// } +/// } +/// } +/// +/// async fn function_handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> { +/// // ... do something +/// Ok(()) +/// } +/// ``` #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub(crate) struct Diagnostic<'a> { - pub(crate) error_type: &'a str, - pub(crate) error_message: &'a str, +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` 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, T> From for Diagnostic<'a> +where + T: Display, +{ + fn from(value: T) -> Self { + Diagnostic { + error_type: Cow::Borrowed(std::any::type_name::()), + error_message: Cow::Owned(format!("{value}")), + } + } } /// Client context sent by the AWS Mobile SDK. @@ -315,8 +371,8 @@ mod test { }); let actual = Diagnostic { - error_type: "InvalidEventDataError", - error_message: "Error parsing event data.", + error_type: Cow::Borrowed("InvalidEventDataError"), + error_message: Cow::Borrowed("Error parsing event data."), }; let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic"); assert_eq!(expected, actual);