error.rs (5530B)
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: Option<Box<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: None, 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 let headers = self.headers.get_or_insert_default(); 79 headers.append(key, value); 80 self 81 } 82 } 83 84 impl From<sqlx::Error> for ApiError { 85 fn from(value: sqlx::Error) -> Self { 86 let (code, status) = match value { 87 sqlx::Error::Configuration(_) => { 88 (ErrorCode::GENERIC_DB_SETUP_FAILED, StatusCode::BAD_GATEWAY) 89 } 90 sqlx::Error::Database(_) | sqlx::Error::Io(_) | sqlx::Error::Tls(_) => { 91 (ErrorCode::GENERIC_DB_FETCH_FAILED, StatusCode::BAD_GATEWAY) 92 } 93 sqlx::Error::PoolTimedOut => todo!(), 94 _ => ( 95 ErrorCode::BANK_UNMANAGED_EXCEPTION, 96 StatusCode::INTERNAL_SERVER_ERROR, 97 ), 98 }; 99 Self { 100 code, 101 hint: None, 102 status: Some(status), 103 log: Some(format!("db: {value}").into_boxed_str()), 104 path: None, 105 headers: None, 106 } 107 } 108 } 109 110 impl From<PaytoErr> for ApiError { 111 fn from(value: PaytoErr) -> Self { 112 ApiError::new(ErrorCode::GENERIC_PAYTO_URI_MALFORMED).with_hint(value.to_string()) 113 } 114 } 115 116 impl From<ParamsErr> for ApiError { 117 fn from(value: ParamsErr) -> Self { 118 ApiError::new(ErrorCode::GENERIC_PARAMETER_MALFORMED) 119 .with_hint(value.to_string()) 120 .with_path(value.param) 121 } 122 } 123 124 impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError { 125 fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self { 126 ApiError::new(ErrorCode::GENERIC_JSON_INVALID) 127 .with_hint(value.inner().to_string()) 128 .with_path(value.path().to_string()) 129 .with_log(value.to_string()) 130 } 131 } 132 133 impl IntoResponse for ApiError { 134 fn into_response(self) -> Response { 135 let status_code = self.status.unwrap_or_else(|| { 136 let (status_code, _) = self.code.metadata(); 137 StatusCode::from_u16(status_code).expect("Invalid status code") 138 }); 139 let log = self.log.or(self.hint.clone()); 140 141 let mut resp = ( 142 status_code, 143 Json(ErrorDetail { 144 code: self.code as u32, 145 hint: self.hint, 146 detail: None, 147 parameter: None, 148 path: self.path, 149 offset: None, 150 index: None, 151 object: None, 152 currency: None, 153 type_expected: None, 154 type_actual: None, 155 extra: None, 156 }), 157 ) 158 .into_response(); 159 if let Some(headers) = self.headers { 160 for (k, v) in *headers { 161 resp.headers_mut().append(k.unwrap(), v); 162 } 163 } 164 if let Some(log) = log { 165 resp.extensions_mut().insert(log); 166 }; 167 168 resp 169 } 170 } 171 172 pub fn failure_code(code: ErrorCode) -> ApiError { 173 ApiError::new(code) 174 } 175 176 pub fn failure(code: ErrorCode, hint: impl Into<Box<str>>) -> ApiError { 177 ApiError::new(code).with_hint(hint) 178 } 179 180 pub fn failure_status(code: ErrorCode, hint: impl Into<Box<str>>, status: StatusCode) -> ApiError { 181 ApiError::new(code).with_hint(hint).with_status(status) 182 } 183 184 pub fn not_implemented(hint: impl Into<Box<str>>) -> ApiError { 185 ApiError::new(ErrorCode::END) 186 .with_hint(hint) 187 .with_status(StatusCode::NOT_IMPLEMENTED) 188 }