error.rs (7139B)
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_common::ErrorDetail, api_params::ParamsErr, error_code::ErrorCode, types::payto::PaytoErr, 27 }; 28 29 pub type ApiResult<T> = Result<T, ApiError>; 30 31 #[derive(Debug)] 32 pub struct ApiError { 33 code: ErrorCode, 34 hint: Option<Box<str>>, 35 log: Option<Box<str>>, 36 status: Option<StatusCode>, 37 path: Option<Box<str>>, 38 headers: Option<Box<HeaderMap>>, 39 } 40 41 impl ApiError { 42 pub fn new(code: ErrorCode) -> Self { 43 Self { 44 code, 45 hint: None, 46 log: None, 47 status: None, 48 path: None, 49 headers: None, 50 } 51 } 52 53 pub fn with_hint(self, hint: impl Display) -> Self { 54 Self { 55 hint: Some(hint.to_string().into_boxed_str()), 56 ..self 57 } 58 } 59 60 pub fn with_log(self, log: impl Into<Box<str>>) -> Self { 61 Self { 62 log: Some(log.into()), 63 ..self 64 } 65 } 66 67 pub fn with_status(self, code: StatusCode) -> Self { 68 Self { 69 status: Some(code), 70 ..self 71 } 72 } 73 74 pub fn with_path(self, path: impl Display) -> Self { 75 Self { 76 path: Some(path.to_string().into_boxed_str()), 77 ..self 78 } 79 } 80 81 pub fn with_header(mut self, key: impl IntoHeaderName, value: HeaderValue) -> Self { 82 let headers = self.headers.get_or_insert_default(); 83 headers.append(key, value); 84 self 85 } 86 } 87 88 impl From<sqlx::Error> for ApiError { 89 fn from(value: sqlx::Error) -> Self { 90 let (code, status) = match value { 91 sqlx::Error::Configuration(_) => { 92 (ErrorCode::GENERIC_DB_SETUP_FAILED, StatusCode::BAD_GATEWAY) 93 } 94 sqlx::Error::Database(_) | sqlx::Error::Io(_) | sqlx::Error::Tls(_) => { 95 (ErrorCode::GENERIC_DB_FETCH_FAILED, StatusCode::BAD_GATEWAY) 96 } 97 sqlx::Error::PoolTimedOut => todo!(), 98 _ => ( 99 ErrorCode::BANK_UNMANAGED_EXCEPTION, 100 StatusCode::INTERNAL_SERVER_ERROR, 101 ), 102 }; 103 Self { 104 code, 105 hint: None, 106 status: Some(status), 107 log: Some(format!("db: {value}").into_boxed_str()), 108 path: None, 109 headers: None, 110 } 111 } 112 } 113 114 impl From<PaytoErr> for ApiError { 115 fn from(value: PaytoErr) -> Self { 116 failure(ErrorCode::GENERIC_PAYTO_URI_MALFORMED, value) 117 } 118 } 119 120 impl From<ParamsErr> for ApiError { 121 fn from(value: ParamsErr) -> Self { 122 failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, &value).with_path(value.param) 123 } 124 } 125 126 impl From<serde_path_to_error::Error<serde_urlencoded::de::Error>> for ApiError { 127 fn from(value: serde_path_to_error::Error<serde_urlencoded::de::Error>) -> Self { 128 failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, value.inner()) 129 .with_path(value.path().to_string()) 130 .with_log(value.to_string()) 131 } 132 } 133 134 impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError { 135 fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self { 136 failure(ErrorCode::GENERIC_JSON_INVALID, value.inner()) 137 .with_path(value.path().to_string()) 138 .with_log(value.to_string()) 139 } 140 } 141 142 impl From<PathRejection> for ApiError { 143 fn from(value: PathRejection) -> Self { 144 match value { 145 PathRejection::FailedToDeserializePathParams(err) => { 146 let kind = err.into_kind(); 147 let err = failure(ErrorCode::GENERIC_PATH_SEGMENT_MALFORMED, &kind); 148 match kind { 149 ErrorKind::ParseErrorAtKey { key, .. } 150 | ErrorKind::InvalidUtf8InPathParam { key } 151 | ErrorKind::DeserializeError { key, .. } => err.with_path(key), 152 ErrorKind::ParseErrorAtIndex { index, .. } => err.with_path(index), 153 _ => err, 154 } 155 } 156 PathRejection::MissingPathParams(err) => { 157 failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, err) 158 } 159 _ => failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, value), 160 } 161 } 162 } 163 164 impl IntoResponse for ApiError { 165 fn into_response(self) -> Response { 166 let status_code = self.status.unwrap_or_else(|| { 167 let (status_code, _) = self.code.metadata(); 168 StatusCode::from_u16(status_code).expect("Invalid status code") 169 }); 170 let log = self.log.or(self.hint.clone()); 171 172 let mut resp = ( 173 status_code, 174 Json(ErrorDetail { 175 code: self.code as u32, 176 hint: self.hint, 177 detail: None, 178 parameter: None, 179 path: self.path, 180 offset: None, 181 index: None, 182 object: None, 183 currency: None, 184 type_expected: None, 185 type_actual: None, 186 extra: None, 187 }), 188 ) 189 .into_response(); 190 if let Some(headers) = self.headers { 191 for (k, v) in *headers { 192 resp.headers_mut().append(k.unwrap(), v); 193 } 194 } 195 if let Some(log) = log { 196 resp.extensions_mut().insert(log); 197 }; 198 199 resp 200 } 201 } 202 203 pub fn failure_code(code: ErrorCode) -> ApiError { 204 ApiError::new(code) 205 } 206 207 pub fn failure(code: ErrorCode, hint: impl Display) -> ApiError { 208 ApiError::new(code).with_hint(hint) 209 } 210 211 pub fn failure_status(code: ErrorCode, hint: impl Display, status: StatusCode) -> ApiError { 212 ApiError::new(code).with_hint(hint).with_status(status) 213 } 214 215 pub fn not_implemented(hint: impl Display) -> ApiError { 216 ApiError::new(ErrorCode::END) 217 .with_hint(hint) 218 .with_status(StatusCode::NOT_IMPLEMENTED) 219 } 220 221 pub fn unauthorized(hint: impl Display) -> ApiError { 222 ApiError::new(ErrorCode::GENERIC_UNAUTHORIZED).with_hint(hint) 223 } 224 225 pub fn forbidden(hint: impl Display) -> ApiError { 226 ApiError::new(ErrorCode::GENERIC_FORBIDDEN).with_hint(hint) 227 } 228 229 pub fn bad_request(hint: impl Display) -> ApiError { 230 ApiError::new(ErrorCode::GENERIC_JSON_INVALID).with_hint(hint) 231 }