1use crate::{HeaderName, HttpResponse, StatusCode};
2
3use aws_lambda_events::encodings::Body;
4use backtrace::Backtrace as _Backtrace;
7use headers::{ContentType, Header};
8use itertools::Itertools;
9use log::error;
10use thiserror::Error;
11
12use std::borrow::Cow;
13use std::string::FromUtf8Error;
14
15#[non_exhaustive]
17#[derive(Debug, Error)]
18pub enum EventError {
19 #[error("failed to prepare HTTP response")]
21 HttpResponse(#[source] Box<http::Error>, _Backtrace),
22 #[error("invalid base64 encoding for request body")]
25 InvalidBodyBase64(#[source] Box<base64::DecodeError>, _Backtrace),
26 #[error("failed to JSON deserialize request body")]
28 InvalidBodyJson(
29 #[source] Box<serde_path_to_error::Error<serde_json::Error>>,
30 _Backtrace,
31 ),
32 #[error("invalid UTF-8 encoding for request body")]
34 InvalidBodyUtf8(#[source] Box<FromUtf8Error>, _Backtrace),
35 #[error("invalid UTF-8 encoding for request header `{0}`")]
37 InvalidHeaderUtf8(
38 HeaderName,
39 #[source] Box<dyn std::error::Error + Send + Sync + 'static>,
42 _Backtrace,
43 ),
44 #[error("failed to parse request path parameter `{param_name}`")]
46 InvalidRequestPathParam {
47 param_name: Cow<'static, str>,
49 #[source]
51 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
52 backtrace: _Backtrace,
54 },
55 #[error("failed to parse request query param `{param_name}`")]
57 InvalidRequestQueryParam {
58 param_name: Cow<'static, str>,
60 #[source]
62 source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
63 backtrace: _Backtrace,
65 },
66 #[error("missing required request body")]
68 MissingRequestBody(_Backtrace),
69 #[error("missing required request header `{0}`")]
71 MissingRequestHeader(Cow<'static, str>, _Backtrace),
72 #[error("missing required request param `{0}`")]
74 MissingRequestParam(Cow<'static, str>, _Backtrace),
75 #[error("request handler panicked: {0}")]
77 Panic(String, _Backtrace),
78 #[error("failed to serialize {type_name} response to JSON")]
80 ToJsonResponse {
81 type_name: Cow<'static, str>,
83 #[source]
85 source: Box<serde_path_to_error::Error<serde_json::Error>>,
86 backtrace: _Backtrace,
88 },
89 #[error("unexpected Content-Type `{0}`")]
91 UnexpectedContentType(String, _Backtrace),
92 #[error("unexpected operation ID: {0}")]
94 UnexpectedOperationId(String, _Backtrace),
95}
96
97impl EventError {
98 pub fn backtrace(&self) -> Option<&_Backtrace> {
100 match self {
101 EventError::HttpResponse(_, backtrace)
102 | EventError::InvalidBodyBase64(_, backtrace)
103 | EventError::InvalidBodyJson(_, backtrace)
104 | EventError::InvalidBodyUtf8(_, backtrace)
105 | EventError::InvalidHeaderUtf8(_, _, backtrace)
106 | EventError::InvalidRequestPathParam { backtrace, .. }
107 | EventError::InvalidRequestQueryParam { backtrace, .. }
108 | EventError::MissingRequestBody(backtrace)
109 | EventError::MissingRequestHeader(_, backtrace)
110 | EventError::MissingRequestParam(_, backtrace)
111 | EventError::Panic(_, backtrace)
112 | EventError::ToJsonResponse { backtrace, .. }
113 | EventError::UnexpectedContentType(_, backtrace)
114 | EventError::UnexpectedOperationId(_, backtrace) => Some(backtrace),
115 }
116 }
117
118 pub fn name(&self) -> &str {
120 match self {
121 EventError::HttpResponse(_, _) => "HttpResponse",
122 EventError::InvalidBodyBase64(_, _) => "InvalidBodyBase64",
123 EventError::InvalidBodyJson(_, _) => "InvalidBodyJson",
124 EventError::InvalidBodyUtf8(_, _) => "InvalidBodyUtf8",
125 EventError::InvalidHeaderUtf8(_, _, _) => "InvalidHeaderUtf8",
126 EventError::InvalidRequestPathParam { .. } => "InvalidRequestPathParam",
127 EventError::InvalidRequestQueryParam { .. } => "InvalidRequestQueryParam",
128 EventError::MissingRequestBody(_) => "MissingRequestBody",
129 EventError::MissingRequestHeader(_, _) => "MissingRequestHeader",
130 EventError::MissingRequestParam(_, _) => "MissingRequestParam",
131 EventError::Panic(_, _) => "Panic",
132 EventError::ToJsonResponse { .. } => "ToJsonResponse",
133 EventError::UnexpectedContentType(_, _) => "UnexpectedContentType",
134 EventError::UnexpectedOperationId(_, _) => "UnexpectedOperationId",
135 }
136 }
137}
138
139impl From<EventError> for HttpResponse {
141 fn from(err: EventError) -> HttpResponse {
142 (&err).into()
143 }
144}
145
146impl From<&EventError> for HttpResponse {
147 fn from(err: &EventError) -> HttpResponse {
155 let (status_code, body) = match err {
156 EventError::InvalidBodyJson(err, _) => (
158 StatusCode::BAD_REQUEST,
159 Some(if err.path().iter().next().is_none() {
161 format!("Invalid request body: {}", err.inner())
162 } else {
163 format!(
164 "Invalid request body (path: `{}`): {}",
165 err.path(),
166 err.inner()
167 )
168 }),
169 ),
170 EventError::InvalidBodyUtf8(_, _) => (
171 StatusCode::BAD_REQUEST,
172 Some("Request body must be UTF-8 encoded".to_string()),
173 ),
174 EventError::InvalidHeaderUtf8(header_name, _, _) => (
175 StatusCode::BAD_REQUEST,
176 Some(format!(
177 "Invalid value for header `{header_name}`: must be UTF-8 encoded"
178 )),
179 ),
180 EventError::InvalidRequestPathParam { param_name, .. } => (
181 StatusCode::BAD_REQUEST,
182 Some(format!("Invalid `{param_name}` request path parameter")),
183 ),
184 EventError::InvalidRequestQueryParam { param_name, .. } => (
185 StatusCode::BAD_REQUEST,
186 Some(format!("Invalid `{param_name}` query parameter")),
187 ),
188 EventError::MissingRequestBody(_) => (
189 StatusCode::BAD_REQUEST,
190 Some("Missing request body".to_string()),
191 ),
192 EventError::MissingRequestHeader(header_name, _) => (
193 StatusCode::BAD_REQUEST,
194 Some(format!("Missing request header `{header_name}`")),
195 ),
196 EventError::MissingRequestParam(param_name, _) => (
197 StatusCode::BAD_REQUEST,
198 Some(format!("Missing required parameter `{param_name}`")),
199 ),
200 EventError::UnexpectedContentType(content_type, _) => (
201 StatusCode::BAD_REQUEST,
202 Some(format!("Unexpected content type `{content_type}`")),
203 ),
204 EventError::HttpResponse(_, _)
206 | EventError::InvalidBodyBase64(_, _)
207 | EventError::Panic(_, _)
208 | EventError::ToJsonResponse { .. }
209 | EventError::UnexpectedOperationId(_, _) => (StatusCode::INTERNAL_SERVER_ERROR, None),
210 };
211
212 let mut response = if let Some(body_str) = body {
213 error!("Responding with error status {status_code}: {body_str}");
214
215 let mut response = HttpResponse::new(Body::Text(body_str));
216 response.headers_mut().insert(
217 ContentType::name().to_owned(),
218 ContentType::text()
219 .to_string()
220 .try_into()
221 .expect("MIME type should be a valid header"),
222 );
223
224 response
225 } else {
226 error!("Responding with error status {status_code}");
227
228 HttpResponse::new(Body::Empty)
229 };
230
231 *response.status_mut() = status_code;
232
233 response
234 }
235}
236
237pub fn format_error(
250 err: &(dyn std::error::Error),
251 name: Option<&str>,
252 backtrace: Option<&_Backtrace>,
253) -> String {
254 let err_line = name
255 .map(|n| format!("{}: {}", n, err))
256 .unwrap_or_else(|| err.to_string());
257
258 let top_error = if let Some(bt) = backtrace {
259 format!("{err_line}\n stack trace:\n{}", format_backtrace(bt, 4))
260 } else {
261 err_line
262 };
263
264 let cause_str = ErrorCauseIterator(err.source())
265 .map(|cause| format!(" caused by: {cause}"))
266 .join("\n");
267
268 if !cause_str.is_empty() {
269 format!("{top_error}\n{cause_str}")
270 } else {
271 top_error
272 }
273}
274
275struct ErrorCauseIterator<'a>(Option<&'a (dyn std::error::Error + 'static)>);
276
277impl<'a> Iterator for ErrorCauseIterator<'a> {
278 type Item = &'a (dyn std::error::Error + 'static);
279
280 fn next(&mut self) -> Option<Self::Item> {
281 let current = self.0;
282 self.0 = current.and_then(|err| err.source());
283 current
284 }
285}
286
287fn format_backtrace(backtrace: &_Backtrace, indent: usize) -> String {
288 let indent_str = " ".repeat(indent);
289 format!("{backtrace:?}")
290 .lines()
291 .join(&format!("{indent_str}\n"))
292}