error.rs (7721B)
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<PathRejection> for ApiError { 161 fn from(value: PathRejection) -> Self { 162 match value { 163 PathRejection::FailedToDeserializePathParams(err) => { 164 let kind = err.into_kind(); 165 let err = failure(ErrorCode::GENERIC_PATH_SEGMENT_MALFORMED, &kind); 166 match kind { 167 ErrorKind::ParseErrorAtKey { key, .. } 168 | ErrorKind::InvalidUtf8InPathParam { key } 169 | ErrorKind::DeserializeError { key, .. } => err.with_path(key), 170 ErrorKind::ParseErrorAtIndex { index, .. } => err.with_path(index), 171 _ => err, 172 } 173 } 174 PathRejection::MissingPathParams(err) => { 175 failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, err) 176 } 177 _ => failure(ErrorCode::BANK_UNMANAGED_EXCEPTION, value), 178 } 179 } 180 } 181 182 impl IntoResponse for ApiError { 183 fn into_response(self) -> Response { 184 let ApiError { 185 code, 186 hint, 187 log, 188 status, 189 path, 190 headers, 191 } = self; 192 let status_code = status.unwrap_or_else(|| { 193 StatusCode::from_u16(code.status_code()).expect("Invalid status code") 194 }); 195 let log = log.or(hint.clone()); 196 197 let mut resp = ( 198 status_code, 199 Json(ErrorDetail { 200 code: code as u16, 201 hint, 202 detail: None, 203 parameter: None, 204 path, 205 offset: None, 206 index: None, 207 object: None, 208 currency: None, 209 type_expected: None, 210 type_actual: None, 211 extra: None, 212 }), 213 ) 214 .into_response(); 215 if let Some(headers) = headers { 216 for (k, v) in *headers { 217 resp.headers_mut().append(k.unwrap(), v); 218 } 219 } 220 resp.extensions_mut() 221 .insert(LoggedError { code, info: log }); 222 223 resp 224 } 225 } 226 227 #[derive(Debug, Clone)] 228 pub struct LoggedError { 229 pub code: ErrorCode, 230 pub info: Option<Box<str>>, 231 } 232 233 pub fn failure_code(code: ErrorCode) -> ApiError { 234 ApiError::new(code) 235 } 236 237 pub fn failure(code: ErrorCode, hint: impl Display) -> ApiError { 238 ApiError::new(code).with_hint(hint) 239 } 240 241 pub fn failure_status(code: ErrorCode, hint: impl Display, status: StatusCode) -> ApiError { 242 ApiError::new(code).with_hint(hint).with_status(status) 243 } 244 245 pub fn not_implemented() -> ApiError { 246 ApiError::new(ErrorCode::END).with_status(StatusCode::NOT_IMPLEMENTED) 247 } 248 249 pub fn unauthorized(hint: impl Display) -> ApiError { 250 ApiError::new(ErrorCode::GENERIC_UNAUTHORIZED).with_hint(hint) 251 } 252 253 pub fn forbidden(hint: impl Display) -> ApiError { 254 ApiError::new(ErrorCode::GENERIC_FORBIDDEN).with_hint(hint) 255 } 256 257 pub fn bad_request(hint: impl Display) -> ApiError { 258 ApiError::new(ErrorCode::GENERIC_JSON_INVALID).with_hint(hint) 259 }