openapi_lambda/
error.rs

1use crate::{HeaderName, HttpResponse, StatusCode};
2
3use aws_lambda_events::encodings::Body;
4// Until std::error::Backtrace is fully stabilized, we can't embed a type named `Backtrace` within
5// a thiserror::Error (see https://github.com/dtolnay/thiserror/issues/204).
6use 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/// Error that occurred while processing an AWS Lambda event.
16#[non_exhaustive]
17#[derive(Debug, Error)]
18pub enum EventError {
19  /// Failed to prepare HTTP response.
20  #[error("failed to prepare HTTP response")]
21  HttpResponse(#[source] Box<http::Error>, _Backtrace),
22  /// Invalid base64 encoding for request body.
23  // The base64 encoding comes from AWS, so this is actually an internal error.
24  #[error("invalid base64 encoding for request body")]
25  InvalidBodyBase64(#[source] Box<base64::DecodeError>, _Backtrace),
26  /// Failed to JSON deserialize request body.
27  #[error("failed to JSON deserialize request body")]
28  InvalidBodyJson(
29    #[source] Box<serde_path_to_error::Error<serde_json::Error>>,
30    _Backtrace,
31  ),
32  /// Invalid UTF-8 encoding for request body.
33  #[error("invalid UTF-8 encoding for request body")]
34  InvalidBodyUtf8(#[source] Box<FromUtf8Error>, _Backtrace),
35  /// Invalid UTF-8 encoding for request header.
36  #[error("invalid UTF-8 encoding for request header `{0}`")]
37  InvalidHeaderUtf8(
38    HeaderName,
39    // We don't use `http::header::ToStrError` here since aws_lambda_events uses a different version
40    // of `http`.
41    #[source] Box<dyn std::error::Error + Send + Sync + 'static>,
42    _Backtrace,
43  ),
44  /// Failed to parse request path parameter.
45  #[error("failed to parse request path parameter `{param_name}`")]
46  InvalidRequestPathParam {
47    /// Name of the parameter that failed to parse.
48    param_name: Cow<'static, str>,
49    /// Underlying error that occurred while parsing the param.
50    #[source]
51    source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
52    /// Stack trace indicating where the error occurred.
53    backtrace: _Backtrace,
54  },
55  /// Failed to parse request query param.
56  #[error("failed to parse request query param `{param_name}`")]
57  InvalidRequestQueryParam {
58    /// Name of the parameter that failed to parse.
59    param_name: Cow<'static, str>,
60    /// Underlying error that occurred while parsing the param.
61    #[source]
62    source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
63    /// Stack trace indicating where the error occurred.
64    backtrace: _Backtrace,
65  },
66  /// Missing required request body.
67  #[error("missing required request body")]
68  MissingRequestBody(_Backtrace),
69  /// Missing required request header.
70  #[error("missing required request header `{0}`")]
71  MissingRequestHeader(Cow<'static, str>, _Backtrace),
72  /// Missing required request param.
73  #[error("missing required request param `{0}`")]
74  MissingRequestParam(Cow<'static, str>, _Backtrace),
75  /// Request handler panicked.
76  #[error("request handler panicked: {0}")]
77  Panic(String, _Backtrace),
78  /// Failed to serialize response body to JSON.
79  #[error("failed to serialize {type_name} response to JSON")]
80  ToJsonResponse {
81    /// Name of the response body type that failed to serialize.
82    type_name: Cow<'static, str>,
83    /// Underlying error that occurred while serializing the response body.
84    #[source]
85    source: Box<serde_path_to_error::Error<serde_json::Error>>,
86    /// Stack trace indicating where the error occurred.
87    backtrace: _Backtrace,
88  },
89  /// Unexpected request Content-Type.
90  #[error("unexpected Content-Type `{0}`")]
91  UnexpectedContentType(String, _Backtrace),
92  /// Unexpected operation ID.
93  #[error("unexpected operation ID: {0}")]
94  UnexpectedOperationId(String, _Backtrace),
95}
96
97impl EventError {
98  /// Return the backtrace associated with the error, if known.
99  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  /// Return the name of the error variant (e.g., `InvalidBodyBase64`).
119  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
139// For convenience.
140impl From<EventError> for HttpResponse {
141  fn from(err: EventError) -> HttpResponse {
142    (&err).into()
143  }
144}
145
146impl From<&EventError> for HttpResponse {
147  /// Build a client-facing [`HttpResponse`] appropriate for the error that occurred.
148  ///
149  /// This function will set the appropriate HTTP status code (400 or 500) depending on whether the
150  /// error is internal (500) or caused by the client (400). For client errors, the
151  /// response body contains a human-readable description of the error and the `Content-Type`
152  /// response header is set to `text/plain`. For internal errors, no response body is returned to
153  /// the client.
154  fn from(err: &EventError) -> HttpResponse {
155    let (status_code, body) = match err {
156      // 400
157      EventError::InvalidBodyJson(err, _) => (
158        StatusCode::BAD_REQUEST,
159        // We expose parse errors to the client to provide better 400 Bad Request diagnostics.
160        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      // 500
205      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
237/// Helper function for formatting an error as a string containing a human-readable chain of causes.
238///
239/// This function will walk over the chain of causes returned by
240/// [`Error::source`](std::error::Error::source) and append each underlying error (using the
241/// [`Display`](std::fmt::Display) trait).
242///
243/// # Arguments
244///
245/// * `err` - Error to format.
246/// * `name` - Optional name of the error type/variant (e.g., `EventError::InvalidBodyJson`).
247/// * `backtrace` - Optional [`Backtrace`](backtrace::Backtrace) indicating where the top-level
248///   error occurred.
249pub 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}