taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

error.rs (7907B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::fmt::Display;
     18 
     19 use axum::{
     20     Json,
     21     extract::{path::ErrorKind, rejection::PathRejection},
     22     http::{HeaderMap, HeaderValue, StatusCode, header::IntoHeaderName},
     23     response::{IntoResponse, Response},
     24 };
     25 use taler_common::{
     26     api::{ErrorDetail, params::ParamsErr},
     27     error_code::ErrorCode,
     28     types::payto::PaytoErr,
     29 };
     30 
     31 pub type ApiResult<T> = Result<T, ApiError>;
     32 
     33 #[derive(Debug, thiserror::Error)]
     34 pub struct ApiError {
     35     code: ErrorCode,
     36     hint: Option<Box<str>>,
     37     log: Option<Box<str>>,
     38     status: Option<StatusCode>,
     39     path: Option<Box<str>>,
     40     headers: Option<Box<HeaderMap>>,
     41 }
     42 
     43 impl Display for ApiError {
     44     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     45         if let Some(h) = &self.hint {
     46             f.write_str(h)
     47         } else {
     48             f.write_str(self.code.description())
     49         }
     50     }
     51 }
     52 
     53 impl ApiError {
     54     pub fn new(code: ErrorCode) -> Self {
     55         Self {
     56             code,
     57             hint: None,
     58             log: None,
     59             status: None,
     60             path: None,
     61             headers: None,
     62         }
     63     }
     64 
     65     pub fn with_hint(self, hint: impl Display) -> Self {
     66         Self {
     67             hint: Some(hint.to_string().into_boxed_str()),
     68             ..self
     69         }
     70     }
     71 
     72     pub fn with_log(self, log: impl Into<Box<str>>) -> Self {
     73         Self {
     74             log: Some(log.into()),
     75             ..self
     76         }
     77     }
     78 
     79     pub fn with_status(self, code: StatusCode) -> Self {
     80         Self {
     81             status: Some(code),
     82             ..self
     83         }
     84     }
     85 
     86     pub fn with_path(self, path: impl Display) -> Self {
     87         Self {
     88             path: Some(path.to_string().into_boxed_str()),
     89             ..self
     90         }
     91     }
     92 
     93     pub fn with_header(mut self, key: impl IntoHeaderName, value: HeaderValue) -> Self {
     94         let headers = self.headers.get_or_insert_default();
     95         headers.append(key, value);
     96         self
     97     }
     98 }
     99 
    100 impl From<sqlx::Error> for ApiError {
    101     fn from(value: sqlx::Error) -> Self {
    102         let (code, status) = match value {
    103             sqlx::Error::Configuration(_) => {
    104                 (ErrorCode::GENERIC_DB_SETUP_FAILED, StatusCode::BAD_GATEWAY)
    105             }
    106             sqlx::Error::Database(_)
    107             | sqlx::Error::Io(_)
    108             | sqlx::Error::Tls(_)
    109             | sqlx::Error::PoolTimedOut => {
    110                 (ErrorCode::GENERIC_DB_FETCH_FAILED, StatusCode::BAD_GATEWAY)
    111             }
    112             _ => (ErrorCode::GENERIC_DB_FETCH_FAILED, StatusCode::BAD_GATEWAY),
    113         };
    114         Self {
    115             code,
    116             hint: None,
    117             status: Some(status),
    118             log: Some(format!("{value}").into_boxed_str()),
    119             path: None,
    120             headers: None,
    121         }
    122     }
    123 }
    124 
    125 impl From<PaytoErr> for ApiError {
    126     fn from(value: PaytoErr) -> Self {
    127         failure(ErrorCode::GENERIC_PAYTO_URI_MALFORMED, value)
    128     }
    129 }
    130 
    131 impl From<ParamsErr> for ApiError {
    132     fn from(value: ParamsErr) -> Self {
    133         failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, &value).with_path(value.param)
    134     }
    135 }
    136 
    137 impl From<serde_path_to_error::Error<serde_urlencoded::de::Error>> for ApiError {
    138     fn from(value: serde_path_to_error::Error<serde_urlencoded::de::Error>) -> Self {
    139         let fmt = value.to_string();
    140         if fmt.contains("missing") {
    141             failure(ErrorCode::GENERIC_PARAMETER_MISSING, value.inner())
    142                 .with_path(value.path().to_string())
    143                 .with_log(fmt)
    144         } else {
    145             failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, value.inner())
    146                 .with_path(value.path().to_string())
    147                 .with_log(fmt)
    148         }
    149     }
    150 }
    151 
    152 impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError {
    153     fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self {
    154         failure(ErrorCode::GENERIC_JSON_INVALID, value.inner())
    155             .with_path(value.path().to_string())
    156             .with_log(value.to_string())
    157     }
    158 }
    159 
    160 impl From<serde_json::Error> for ApiError {
    161     fn from(value: serde_json::Error) -> Self {
    162         failure(ErrorCode::GENERIC_JSON_INVALID, &value).with_log(value.to_string())
    163     }
    164 }
    165 
    166 impl From<PathRejection> for ApiError {
    167     fn from(value: PathRejection) -> Self {
    168         match value {
    169             PathRejection::FailedToDeserializePathParams(err) => {
    170                 let kind = err.into_kind();
    171                 let err = failure(ErrorCode::GENERIC_PATH_SEGMENT_MALFORMED, &kind);
    172                 match kind {
    173                     ErrorKind::ParseErrorAtKey { key, .. }
    174                     | ErrorKind::InvalidUtf8InPathParam { key }
    175                     | ErrorKind::DeserializeError { key, .. } => err.with_path(key),
    176                     ErrorKind::ParseErrorAtIndex { index, .. } => err.with_path(index),
    177                     _ => err,
    178                 }
    179             }
    180             PathRejection::MissingPathParams(err) => {
    181                 failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, err)
    182             }
    183             _ => failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, value),
    184         }
    185     }
    186 }
    187 
    188 impl IntoResponse for ApiError {
    189     fn into_response(self) -> Response {
    190         let ApiError {
    191             code,
    192             hint,
    193             log,
    194             status,
    195             path,
    196             headers,
    197         } = self;
    198         let status_code = status.unwrap_or_else(|| {
    199             StatusCode::from_u16(code.status_code()).expect("Invalid status code")
    200         });
    201         let log = log.or(hint.clone());
    202 
    203         let mut resp = (
    204             status_code,
    205             Json(ErrorDetail {
    206                 code: code as u16,
    207                 hint,
    208                 detail: None,
    209                 parameter: None,
    210                 path,
    211                 offset: None,
    212                 index: None,
    213                 object: None,
    214                 currency: None,
    215                 type_expected: None,
    216                 type_actual: None,
    217                 extra: None,
    218             }),
    219         )
    220             .into_response();
    221         if let Some(headers) = headers {
    222             for (k, v) in *headers {
    223                 resp.headers_mut().append(k.unwrap(), v);
    224             }
    225         }
    226         resp.extensions_mut()
    227             .insert(LoggedError { code, info: log });
    228 
    229         resp
    230     }
    231 }
    232 
    233 #[derive(Debug, Clone)]
    234 pub struct LoggedError {
    235     pub code: ErrorCode,
    236     pub info: Option<Box<str>>,
    237 }
    238 
    239 pub fn failure_code(code: ErrorCode) -> ApiError {
    240     ApiError::new(code)
    241 }
    242 
    243 pub fn failure(code: ErrorCode, hint: impl Display) -> ApiError {
    244     ApiError::new(code).with_hint(hint)
    245 }
    246 
    247 pub fn failure_status(code: ErrorCode, hint: impl Display, status: StatusCode) -> ApiError {
    248     ApiError::new(code).with_hint(hint).with_status(status)
    249 }
    250 
    251 pub fn not_implemented() -> ApiError {
    252     ApiError::new(ErrorCode::END).with_status(StatusCode::NOT_IMPLEMENTED)
    253 }
    254 
    255 pub fn unauthorized(hint: impl Display) -> ApiError {
    256     ApiError::new(ErrorCode::GENERIC_UNAUTHORIZED).with_hint(hint)
    257 }
    258 
    259 pub fn forbidden(hint: impl Display) -> ApiError {
    260     ApiError::new(ErrorCode::GENERIC_FORBIDDEN).with_hint(hint)
    261 }
    262 
    263 pub fn bad_request(hint: impl Display) -> ApiError {
    264     ApiError::new(ErrorCode::GENERIC_JSON_INVALID).with_hint(hint)
    265 }