LCOV - code coverage report
Current view: top level - libs/http-utils/src - error.rs (source / functions) Coverage Total Hit
Test: 5fe7fa8d483b39476409aee736d6d5e32728bfac.info Lines: 0.0 % 89 0
Test Date: 2025-03-12 16:10:49 Functions: 0.0 % 16 0

            Line data    Source code
       1              : use std::borrow::Cow;
       2              : use std::error::Error as StdError;
       3              : 
       4              : use hyper::{Body, Response, StatusCode, header};
       5              : use serde::{Deserialize, Serialize};
       6              : use thiserror::Error;
       7              : use tracing::{error, info, warn};
       8              : use utils::auth::AuthError;
       9              : 
      10              : #[derive(Debug, Error)]
      11              : pub enum ApiError {
      12              :     #[error("Bad request: {0:#?}")]
      13              :     BadRequest(anyhow::Error),
      14              : 
      15              :     #[error("Forbidden: {0}")]
      16              :     Forbidden(String),
      17              : 
      18              :     #[error("Unauthorized: {0}")]
      19              :     Unauthorized(String),
      20              : 
      21              :     #[error("NotFound: {0}")]
      22              :     NotFound(Box<dyn StdError + Send + Sync + 'static>),
      23              : 
      24              :     #[error("Conflict: {0}")]
      25              :     Conflict(String),
      26              : 
      27              :     #[error("Precondition failed: {0}")]
      28              :     PreconditionFailed(Box<str>),
      29              : 
      30              :     #[error("Resource temporarily unavailable: {0}")]
      31              :     ResourceUnavailable(Cow<'static, str>),
      32              : 
      33              :     #[error("Too many requests: {0}")]
      34              :     TooManyRequests(Cow<'static, str>),
      35              : 
      36              :     #[error("Shutting down")]
      37              :     ShuttingDown,
      38              : 
      39              :     #[error("Timeout")]
      40              :     Timeout(Cow<'static, str>),
      41              : 
      42              :     #[error("Request cancelled")]
      43              :     Cancelled,
      44              : 
      45              :     #[error(transparent)]
      46              :     InternalServerError(anyhow::Error),
      47              : }
      48              : 
      49              : impl ApiError {
      50            0 :     pub fn into_response(self) -> Response<Body> {
      51            0 :         match self {
      52            0 :             ApiError::BadRequest(err) => HttpErrorBody::response_from_msg_and_status(
      53            0 :                 format!("{err:#?}"), // use debug printing so that we give the cause
      54            0 :                 StatusCode::BAD_REQUEST,
      55            0 :             ),
      56              :             ApiError::Forbidden(_) => {
      57            0 :                 HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::FORBIDDEN)
      58              :             }
      59            0 :             ApiError::Unauthorized(_) => HttpErrorBody::response_from_msg_and_status(
      60            0 :                 self.to_string(),
      61            0 :                 StatusCode::UNAUTHORIZED,
      62            0 :             ),
      63              :             ApiError::NotFound(_) => {
      64            0 :                 HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::NOT_FOUND)
      65              :             }
      66              :             ApiError::Conflict(_) => {
      67            0 :                 HttpErrorBody::response_from_msg_and_status(self.to_string(), StatusCode::CONFLICT)
      68              :             }
      69            0 :             ApiError::PreconditionFailed(_) => HttpErrorBody::response_from_msg_and_status(
      70            0 :                 self.to_string(),
      71            0 :                 StatusCode::PRECONDITION_FAILED,
      72            0 :             ),
      73            0 :             ApiError::ShuttingDown => HttpErrorBody::response_from_msg_and_status(
      74            0 :                 "Shutting down".to_string(),
      75            0 :                 StatusCode::SERVICE_UNAVAILABLE,
      76            0 :             ),
      77            0 :             ApiError::ResourceUnavailable(err) => HttpErrorBody::response_from_msg_and_status(
      78            0 :                 err.to_string(),
      79            0 :                 StatusCode::SERVICE_UNAVAILABLE,
      80            0 :             ),
      81            0 :             ApiError::TooManyRequests(err) => HttpErrorBody::response_from_msg_and_status(
      82            0 :                 err.to_string(),
      83            0 :                 StatusCode::TOO_MANY_REQUESTS,
      84            0 :             ),
      85            0 :             ApiError::Timeout(err) => HttpErrorBody::response_from_msg_and_status(
      86            0 :                 err.to_string(),
      87            0 :                 StatusCode::REQUEST_TIMEOUT,
      88            0 :             ),
      89            0 :             ApiError::Cancelled => HttpErrorBody::response_from_msg_and_status(
      90            0 :                 self.to_string(),
      91            0 :                 StatusCode::INTERNAL_SERVER_ERROR,
      92            0 :             ),
      93            0 :             ApiError::InternalServerError(err) => HttpErrorBody::response_from_msg_and_status(
      94            0 :                 format!("{err:#}"), // use alternative formatting so that we give the cause without backtrace
      95            0 :                 StatusCode::INTERNAL_SERVER_ERROR,
      96            0 :             ),
      97              :         }
      98            0 :     }
      99              : }
     100              : 
     101              : impl From<AuthError> for ApiError {
     102            0 :     fn from(_value: AuthError) -> Self {
     103            0 :         // Don't pass on the value of the AuthError as a precautionary measure.
     104            0 :         // Being intentionally vague in public error communication hurts debugability
     105            0 :         // but it is more secure.
     106            0 :         ApiError::Forbidden("JWT authentication error".to_string())
     107            0 :     }
     108              : }
     109              : 
     110            0 : #[derive(Serialize, Deserialize)]
     111              : pub struct HttpErrorBody {
     112              :     pub msg: String,
     113              : }
     114              : 
     115              : impl HttpErrorBody {
     116            0 :     pub fn from_msg(msg: String) -> Self {
     117            0 :         HttpErrorBody { msg }
     118            0 :     }
     119              : 
     120            0 :     pub fn response_from_msg_and_status(msg: String, status: StatusCode) -> Response<Body> {
     121            0 :         HttpErrorBody { msg }.to_response(status)
     122            0 :     }
     123              : 
     124            0 :     pub fn to_response(&self, status: StatusCode) -> Response<Body> {
     125            0 :         Response::builder()
     126            0 :             .status(status)
     127            0 :             .header(header::CONTENT_TYPE, "application/json")
     128            0 :             // we do not have nested maps with non string keys so serialization shouldn't fail
     129            0 :             .body(Body::from(serde_json::to_string(self).unwrap()))
     130            0 :             .unwrap()
     131            0 :     }
     132              : }
     133              : 
     134            0 : pub async fn route_error_handler(err: routerify::RouteError) -> Response<Body> {
     135            0 :     match err.downcast::<ApiError>() {
     136            0 :         Ok(api_error) => api_error_handler(*api_error),
     137            0 :         Err(other_error) => {
     138            0 :             // We expect all the request handlers to return an ApiError, so this should
     139            0 :             // not be reached. But just in case.
     140            0 :             error!("Error processing HTTP request: {other_error:?}");
     141            0 :             HttpErrorBody::response_from_msg_and_status(
     142            0 :                 other_error.to_string(),
     143            0 :                 StatusCode::INTERNAL_SERVER_ERROR,
     144            0 :             )
     145              :         }
     146              :     }
     147            0 : }
     148              : 
     149            0 : pub fn api_error_handler(api_error: ApiError) -> Response<Body> {
     150            0 :     // Print a stack trace for Internal Server errors
     151            0 : 
     152            0 :     match api_error {
     153              :         ApiError::Forbidden(_) | ApiError::Unauthorized(_) => {
     154            0 :             warn!("Error processing HTTP request: {api_error:#}")
     155              :         }
     156            0 :         ApiError::ResourceUnavailable(_) => info!("Error processing HTTP request: {api_error:#}"),
     157            0 :         ApiError::NotFound(_) => info!("Error processing HTTP request: {api_error:#}"),
     158            0 :         ApiError::InternalServerError(_) => error!("Error processing HTTP request: {api_error:?}"),
     159            0 :         ApiError::ShuttingDown => info!("Shut down while processing HTTP request"),
     160            0 :         ApiError::Timeout(_) => info!("Timeout while processing HTTP request: {api_error:#}"),
     161            0 :         ApiError::Cancelled => info!("Request cancelled while processing HTTP request"),
     162            0 :         _ => info!("Error processing HTTP request: {api_error:#}"),
     163              :     }
     164              : 
     165            0 :     api_error.into_response()
     166            0 : }
        

Generated by: LCOV version 2.1-beta