error.rs (7783B)
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 _ => ( 113 ErrorCode::BANK_UNMANAGED_EXCEPTION, 114 StatusCode::INTERNAL_SERVER_ERROR, 115 ), 116 }; 117 Self { 118 code, 119 hint: None, 120 status: Some(status), 121 log: Some(format!("db: {value}").into_boxed_str()), 122 path: None, 123 headers: None, 124 } 125 } 126 } 127 128 impl From<PaytoErr> for ApiError { 129 fn from(value: PaytoErr) -> Self { 130 failure(ErrorCode::GENERIC_PAYTO_URI_MALFORMED, value) 131 } 132 } 133 134 impl From<ParamsErr> for ApiError { 135 fn from(value: ParamsErr) -> Self { 136 failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, &value).with_path(value.param) 137 } 138 } 139 140 impl From<serde_path_to_error::Error<serde_urlencoded::de::Error>> for ApiError { 141 fn from(value: serde_path_to_error::Error<serde_urlencoded::de::Error>) -> Self { 142 let fmt = value.to_string(); 143 if fmt.contains("missing") { 144 failure(ErrorCode::GENERIC_PARAMETER_MISSING, value.inner()) 145 .with_path(value.path().to_string()) 146 .with_log(fmt) 147 } else { 148 failure(ErrorCode::GENERIC_PARAMETER_MALFORMED, value.inner()) 149 .with_path(value.path().to_string()) 150 .with_log(fmt) 151 } 152 } 153 } 154 155 impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError { 156 fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self { 157 failure(ErrorCode::GENERIC_JSON_INVALID, value.inner()) 158 .with_path(value.path().to_string()) 159 .with_log(value.to_string()) 160 } 161 } 162 163 impl From<PathRejection> for ApiError { 164 fn from(value: PathRejection) -> Self { 165 match value { 166 PathRejection::FailedToDeserializePathParams(err) => { 167 let kind = err.into_kind(); 168 let err = failure(ErrorCode::GENERIC_PATH_SEGMENT_MALFORMED, &kind); 169 match kind { 170 ErrorKind::ParseErrorAtKey { key, .. } 171 | ErrorKind::InvalidUtf8InPathParam { key } 172 | ErrorKind::DeserializeError { key, .. } => err.with_path(key), 173 ErrorKind::ParseErrorAtIndex { index, .. } => err.with_path(index), 174 _ => err, 175 } 176 } 177 PathRejection::MissingPathParams(err) => { 178 failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, err) 179 } 180 _ => failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, value), 181 } 182 } 183 } 184 185 impl IntoResponse for ApiError { 186 fn into_response(self) -> Response { 187 let ApiError { 188 code, 189 hint, 190 log, 191 status, 192 path, 193 headers, 194 } = self; 195 let status_code = status.unwrap_or_else(|| { 196 StatusCode::from_u16(code.status_code()).expect("Invalid status code") 197 }); 198 let log = log.or(hint.clone()); 199 200 let mut resp = ( 201 status_code, 202 Json(ErrorDetail { 203 code: code as u16, 204 hint, 205 detail: None, 206 parameter: None, 207 path, 208 offset: None, 209 index: None, 210 object: None, 211 currency: None, 212 type_expected: None, 213 type_actual: None, 214 extra: None, 215 }), 216 ) 217 .into_response(); 218 if let Some(headers) = headers { 219 for (k, v) in *headers { 220 resp.headers_mut().append(k.unwrap(), v); 221 } 222 } 223 resp.extensions_mut() 224 .insert(LoggedError { code, info: log }); 225 226 resp 227 } 228 } 229 230 #[derive(Debug, Clone)] 231 pub struct LoggedError { 232 pub code: ErrorCode, 233 pub info: Option<Box<str>>, 234 } 235 236 pub fn failure_code(code: ErrorCode) -> ApiError { 237 ApiError::new(code) 238 } 239 240 pub fn failure(code: ErrorCode, hint: impl Display) -> ApiError { 241 ApiError::new(code).with_hint(hint) 242 } 243 244 pub fn failure_status(code: ErrorCode, hint: impl Display, status: StatusCode) -> ApiError { 245 ApiError::new(code).with_hint(hint).with_status(status) 246 } 247 248 pub fn not_implemented() -> ApiError { 249 ApiError::new(ErrorCode::END).with_status(StatusCode::NOT_IMPLEMENTED) 250 } 251 252 pub fn unauthorized(hint: impl Display) -> ApiError { 253 ApiError::new(ErrorCode::GENERIC_UNAUTHORIZED).with_hint(hint) 254 } 255 256 pub fn forbidden(hint: impl Display) -> ApiError { 257 ApiError::new(ErrorCode::GENERIC_FORBIDDEN).with_hint(hint) 258 } 259 260 pub fn bad_request(hint: impl Display) -> ApiError { 261 ApiError::new(ErrorCode::GENERIC_JSON_INVALID).with_hint(hint) 262 }