error.rs (5422B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024-2025 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 axum::{ 18 Json, 19 http::{HeaderMap, HeaderValue, StatusCode, header::IntoHeaderName}, 20 response::{IntoResponse, Response}, 21 }; 22 use taler_common::{ 23 api_common::ErrorDetail, api_params::ParamsErr, error_code::ErrorCode, types::payto::PaytoErr, 24 }; 25 26 pub type ApiResult<T> = Result<T, ApiError>; 27 28 pub struct ApiError { 29 code: ErrorCode, 30 hint: Option<Box<str>>, 31 log: Option<Box<str>>, 32 status: Option<StatusCode>, 33 path: Option<Box<str>>, 34 headers: HeaderMap, 35 } 36 37 impl ApiError { 38 pub fn new(code: ErrorCode) -> Self { 39 Self { 40 code, 41 hint: None, 42 log: None, 43 status: None, 44 path: None, 45 headers: HeaderMap::new(), 46 } 47 } 48 49 pub fn with_hint(self, hint: impl Into<Box<str>>) -> Self { 50 Self { 51 hint: Some(hint.into()), 52 ..self 53 } 54 } 55 56 pub fn with_log(self, log: impl Into<Box<str>>) -> Self { 57 Self { 58 log: Some(log.into()), 59 ..self 60 } 61 } 62 63 pub fn with_status(self, code: StatusCode) -> Self { 64 Self { 65 status: Some(code), 66 ..self 67 } 68 } 69 70 pub fn with_path(self, path: impl Into<Box<str>>) -> Self { 71 Self { 72 path: Some(path.into()), 73 ..self 74 } 75 } 76 77 pub fn with_header(mut self, key: impl IntoHeaderName, value: HeaderValue) -> Self { 78 self.headers.append(key, value); 79 self 80 } 81 } 82 83 impl From<sqlx::Error> for ApiError { 84 fn from(value: sqlx::Error) -> Self { 85 let (code, status) = match value { 86 sqlx::Error::Configuration(_) => { 87 (ErrorCode::GENERIC_DB_SETUP_FAILED, StatusCode::BAD_GATEWAY) 88 } 89 sqlx::Error::Database(_) | sqlx::Error::Io(_) | sqlx::Error::Tls(_) => { 90 (ErrorCode::GENERIC_DB_FETCH_FAILED, StatusCode::BAD_GATEWAY) 91 } 92 sqlx::Error::PoolTimedOut => todo!(), 93 _ => ( 94 ErrorCode::BANK_UNMANAGED_EXCEPTION, 95 StatusCode::INTERNAL_SERVER_ERROR, 96 ), 97 }; 98 Self { 99 code, 100 hint: None, 101 status: Some(status), 102 log: Some(format!("db: {value}").into_boxed_str()), 103 path: None, 104 headers: HeaderMap::new(), 105 } 106 } 107 } 108 109 impl From<PaytoErr> for ApiError { 110 fn from(value: PaytoErr) -> Self { 111 ApiError::new(ErrorCode::GENERIC_PAYTO_URI_MALFORMED).with_hint(value.to_string()) 112 } 113 } 114 115 impl From<ParamsErr> for ApiError { 116 fn from(value: ParamsErr) -> Self { 117 ApiError::new(ErrorCode::GENERIC_PARAMETER_MALFORMED) 118 .with_hint(value.to_string()) 119 .with_path(value.param) 120 } 121 } 122 123 impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError { 124 fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self { 125 ApiError::new(ErrorCode::GENERIC_JSON_INVALID) 126 .with_hint(value.inner().to_string()) 127 .with_path(value.path().to_string()) 128 .with_log(value.to_string()) 129 } 130 } 131 132 impl IntoResponse for ApiError { 133 fn into_response(self) -> Response { 134 let status_code = self.status.unwrap_or_else(|| { 135 let (status_code, _) = self.code.metadata(); 136 StatusCode::from_u16(status_code).expect("Invalid status code") 137 }); 138 let log = self.log.or(self.hint.clone()); 139 140 let mut resp = ( 141 status_code, 142 Json(ErrorDetail { 143 code: self.code as u32, 144 hint: self.hint, 145 detail: None, 146 parameter: None, 147 path: self.path, 148 offset: None, 149 index: None, 150 object: None, 151 currency: None, 152 type_expected: None, 153 type_actual: None, 154 extra: None, 155 }), 156 ) 157 .into_response(); 158 for (k, v) in self.headers { 159 resp.headers_mut().append(k.unwrap(), v); 160 } 161 if let Some(log) = log { 162 resp.extensions_mut().insert(log); 163 }; 164 165 resp 166 } 167 } 168 169 pub fn failure_code(code: ErrorCode) -> ApiError { 170 ApiError::new(code) 171 } 172 173 pub fn failure(code: ErrorCode, hint: impl Into<Box<str>>) -> ApiError { 174 ApiError::new(code).with_hint(hint) 175 } 176 177 pub fn failure_status(code: ErrorCode, hint: impl Into<Box<str>>, status: StatusCode) -> ApiError { 178 ApiError::new(code).with_hint(hint).with_status(status) 179 } 180 181 pub fn not_implemented(hint: impl Into<Box<str>>) -> ApiError { 182 ApiError::new(ErrorCode::END) 183 .with_hint(hint) 184 .with_status(StatusCode::NOT_IMPLEMENTED) 185 }