Skip to content

Commit 387b07c

Browse files
authored
Allow error response customization (#828)
* Feat: custom error type support Error responses of `lambda_runtime` are now customizable. One use case is to avoid using `std::any::type_name` for error types, which is not reliable for determining subsequent program behavior. `F::Error` of `Runtime::run` and `run` is required to implement `Into<Diagnostic<'a>>` instead of `Display`. `EventErrorRequest` accepts a value implementing `Into<Diagnostic<'a>>` instead of the error type and message. `Diagnostic` becomes public because users may implement their own conversions. Its fields are wrapped in `Cow` so that they can carry both borrowed and owned strings. `Diagnostic` is implemented for every type that implements `Display` as a fallback, which also ensures backward compatibility. * Chore: add example to comments on Diagnostic The comments on `Diagnositc` shows how to customize error responses with an example.
1 parent fabffbc commit 387b07c

File tree

3 files changed

+87
-27
lines changed

3 files changed

+87
-27
lines changed

lambda-runtime/src/lib.rs

+21-14
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ use hyper::{body::Incoming, http::Request};
1414
use lambda_runtime_api_client::{body::Body, BoxError, Client};
1515
use serde::{Deserialize, Serialize};
1616
use std::{
17+
borrow::Cow,
1718
env,
18-
fmt::{self, Debug, Display},
19+
fmt::{self, Debug},
1920
future::Future,
2021
panic,
2122
sync::Arc,
@@ -33,7 +34,9 @@ pub mod streaming;
3334
mod types;
3435

3536
use requests::{EventCompletionRequest, EventErrorRequest, IntoRequest, NextEventRequest};
36-
pub use types::{Context, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse};
37+
pub use types::{
38+
Context, Diagnostic, FunctionResponse, IntoFunctionResponse, LambdaEvent, MetadataPrelude, StreamResponse,
39+
};
3740

3841
use types::invoke_request_id;
3942

@@ -96,7 +99,7 @@ impl Runtime {
9699
where
97100
F: Service<LambdaEvent<A>>,
98101
F::Future: Future<Output = Result<R, F::Error>>,
99-
F::Error: fmt::Debug + fmt::Display,
102+
F::Error: for<'a> Into<Diagnostic<'a>> + fmt::Debug,
100103
A: for<'de> Deserialize<'de>,
101104
R: IntoFunctionResponse<B, S>,
102105
B: Serialize,
@@ -173,7 +176,14 @@ impl Runtime {
173176
} else {
174177
"Lambda panicked".to_string()
175178
};
176-
EventErrorRequest::new(request_id, error_type, &msg).into_req()
179+
EventErrorRequest::new(
180+
request_id,
181+
Diagnostic {
182+
error_type: Cow::Borrowed(error_type),
183+
error_message: Cow::Owned(msg),
184+
},
185+
)
186+
.into_req()
177187
}
178188
}
179189
}
@@ -224,7 +234,7 @@ pub async fn run<A, F, R, B, S, D, E>(handler: F) -> Result<(), Error>
224234
where
225235
F: Service<LambdaEvent<A>>,
226236
F::Future: Future<Output = Result<R, F::Error>>,
227-
F::Error: fmt::Debug + fmt::Display,
237+
F::Error: for<'a> Into<Diagnostic<'a>> + fmt::Debug,
228238
A: for<'de> Deserialize<'de>,
229239
R: IntoFunctionResponse<B, S>,
230240
B: Serialize,
@@ -249,15 +259,12 @@ fn type_name_of_val<T>(_: T) -> &'static str {
249259
std::any::type_name::<T>()
250260
}
251261

252-
fn build_event_error_request<T>(request_id: &str, err: T) -> Result<Request<Body>, Error>
262+
fn build_event_error_request<'a, T>(request_id: &'a str, err: T) -> Result<Request<Body>, Error>
253263
where
254-
T: Display + Debug,
264+
T: Into<Diagnostic<'a>> + Debug,
255265
{
256266
error!("{:?}", err); // logs the error in CloudWatch
257-
let error_type = type_name_of_val(&err);
258-
let msg = format!("{err}");
259-
260-
EventErrorRequest::new(request_id, error_type, &msg).into_req()
267+
EventErrorRequest::new(request_id, err).into_req()
261268
}
262269

263270
#[cfg(test)]
@@ -274,7 +281,7 @@ mod endpoint_tests {
274281
use httpmock::prelude::*;
275282

276283
use lambda_runtime_api_client::Client;
277-
use std::{env, sync::Arc};
284+
use std::{borrow::Cow, env, sync::Arc};
278285
use tokio_stream::StreamExt;
279286

280287
#[tokio::test]
@@ -341,8 +348,8 @@ mod endpoint_tests {
341348
#[tokio::test]
342349
async fn test_error_response() -> Result<(), Error> {
343350
let diagnostic = Diagnostic {
344-
error_type: "InvalidEventDataError",
345-
error_message: "Error parsing event data",
351+
error_type: Cow::Borrowed("InvalidEventDataError"),
352+
error_message: Cow::Borrowed("Error parsing event data"),
346353
};
347354
let body = serde_json::to_string(&diagnostic)?;
348355

lambda-runtime/src/requests.rs

+4-7
Original file line numberDiff line numberDiff line change
@@ -194,13 +194,10 @@ pub(crate) struct EventErrorRequest<'a> {
194194
}
195195

196196
impl<'a> EventErrorRequest<'a> {
197-
pub(crate) fn new(request_id: &'a str, error_type: &'a str, error_message: &'a str) -> EventErrorRequest<'a> {
197+
pub(crate) fn new(request_id: &'a str, diagnostic: impl Into<Diagnostic<'a>>) -> EventErrorRequest<'a> {
198198
EventErrorRequest {
199199
request_id,
200-
diagnostic: Diagnostic {
201-
error_type,
202-
error_message,
203-
},
200+
diagnostic: diagnostic.into(),
204201
}
205202
}
206203
}
@@ -226,8 +223,8 @@ fn test_event_error_request() {
226223
let req = EventErrorRequest {
227224
request_id: "id",
228225
diagnostic: Diagnostic {
229-
error_type: "InvalidEventDataError",
230-
error_message: "Error parsing event data",
226+
error_type: std::borrow::Cow::Borrowed("InvalidEventDataError"),
227+
error_message: std::borrow::Cow::Borrowed("Error parsing event data"),
231228
},
232229
};
233230
let req = req.into_req().unwrap();

lambda-runtime/src/types.rs

+62-6
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,75 @@ use http::{header::ToStrError, HeaderMap, HeaderValue, StatusCode};
55
use lambda_runtime_api_client::body::Body;
66
use serde::{Deserialize, Serialize};
77
use std::{
8+
borrow::Cow,
89
collections::HashMap,
910
env,
10-
fmt::Debug,
11+
fmt::{Debug, Display},
1112
time::{Duration, SystemTime},
1213
};
1314
use tokio_stream::Stream;
1415
use tracing::Span;
1516

17+
/// Diagnostic information about an error.
18+
///
19+
/// `Diagnostic` is automatically derived for types that implement
20+
/// [`Display`][std::fmt::Display]; e.g., [`Error`][std::error::Error].
21+
///
22+
/// [`error_type`][`Diagnostic::error_type`] is derived from the type name of
23+
/// the original error with [`std::any::type_name`] as a fallback, which may
24+
/// not be reliable for conditional error handling.
25+
/// You can define your own error container that implements `Into<Diagnostic>`
26+
/// if you need to handle errors based on error types.
27+
///
28+
/// Example:
29+
/// ```
30+
/// use lambda_runtime::{Diagnostic, Error, LambdaEvent};
31+
/// use std::borrow::Cow;
32+
///
33+
/// #[derive(Debug)]
34+
/// struct ErrorResponse(Error);
35+
///
36+
/// impl<'a> Into<Diagnostic<'a>> for ErrorResponse {
37+
/// fn into(self) -> Diagnostic<'a> {
38+
/// Diagnostic {
39+
/// error_type: Cow::Borrowed("MyError"),
40+
/// error_message: Cow::Owned(self.0.to_string()),
41+
/// }
42+
/// }
43+
/// }
44+
///
45+
/// async fn function_handler(_event: LambdaEvent<()>) -> Result<(), ErrorResponse> {
46+
/// // ... do something
47+
/// Ok(())
48+
/// }
49+
/// ```
1650
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
1751
#[serde(rename_all = "camelCase")]
18-
pub(crate) struct Diagnostic<'a> {
19-
pub(crate) error_type: &'a str,
20-
pub(crate) error_message: &'a str,
52+
pub struct Diagnostic<'a> {
53+
/// Error type.
54+
///
55+
/// `error_type` is derived from the type name of the original error with
56+
/// [`std::any::type_name`] as a fallback.
57+
/// Please implement your own `Into<Diagnostic>` if you need more reliable
58+
/// error types.
59+
pub error_type: Cow<'a, str>,
60+
/// Error message.
61+
///
62+
/// `error_message` is the output from the [`Display`][std::fmt::Display]
63+
/// implementation of the original error as a fallback.
64+
pub error_message: Cow<'a, str>,
65+
}
66+
67+
impl<'a, T> From<T> for Diagnostic<'a>
68+
where
69+
T: Display,
70+
{
71+
fn from(value: T) -> Self {
72+
Diagnostic {
73+
error_type: Cow::Borrowed(std::any::type_name::<T>()),
74+
error_message: Cow::Owned(format!("{value}")),
75+
}
76+
}
2177
}
2278

2379
/// Client context sent by the AWS Mobile SDK.
@@ -315,8 +371,8 @@ mod test {
315371
});
316372

317373
let actual = Diagnostic {
318-
error_type: "InvalidEventDataError",
319-
error_message: "Error parsing event data.",
374+
error_type: Cow::Borrowed("InvalidEventDataError"),
375+
error_message: Cow::Borrowed("Error parsing event data."),
320376
};
321377
let actual: Value = serde_json::to_value(actual).expect("failed to serialize diagnostic");
322378
assert_eq!(expected, actual);

0 commit comments

Comments
 (0)