1use axum::{
8 http::StatusCode,
9 response::{IntoResponse, Response},
10 Json,
11};
12use serde::Serialize;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
17pub enum ApiError {
18 #[error("Bad request: {0}")]
19 BadRequest(String),
20
21 #[error("Missing file in request")]
22 MissingFile,
23
24 #[error("File too large: maximum size is {max_mb} MB")]
25 FileTooLarge { max_mb: usize },
26
27 #[error("Multipart error: {0}")]
28 Multipart(#[from] axum::extract::multipart::MultipartError),
29
30 #[error("Processing error: {0}")]
31 Processing(String),
32
33 #[error("Cache error: {0}")]
34 Cache(String),
35
36 #[error("Not found: {0}")]
37 NotFound(String),
38
39 #[error("Internal server error: {0}")]
40 Internal(String),
41
42 #[error("Join error")]
43 Join(#[from] tokio::task::JoinError),
44
45 #[error("Parquet serialization error: {0}")]
46 Parquet(String),
47
48 #[error("Server overloaded, retry after {retry_after_secs}s")]
49 Overloaded { retry_after_secs: u64 },
50}
51
52const CACHE_CLIENT_MESSAGE: &str = "Cache error: the server's cache store failed";
56
57#[derive(Debug, Serialize)]
65pub struct ErrorResponse {
66 pub error: String,
67 pub code: String,
68}
69
70pub fn error_response(status: StatusCode, code: &str, error: impl Into<String>) -> Response {
74 let body = ErrorResponse {
75 error: error.into(),
76 code: code.to_owned(),
77 };
78 (status, Json(body)).into_response()
79}
80
81impl IntoResponse for ApiError {
82 fn into_response(self) -> Response {
83 if let ApiError::FileTooLarge { max_mb } = &self {
84 tracing::warn!(max_mb, "Rejecting oversized upload");
85 }
86
87 let (status, code) = match &self {
88 ApiError::BadRequest(_) => (StatusCode::BAD_REQUEST, "BAD_REQUEST"),
89 ApiError::MissingFile => (StatusCode::BAD_REQUEST, "MISSING_FILE"),
90 ApiError::FileTooLarge { .. } => (StatusCode::PAYLOAD_TOO_LARGE, "FILE_TOO_LARGE"),
91 ApiError::Multipart(_) => (StatusCode::BAD_REQUEST, "MULTIPART_ERROR"),
92 ApiError::Processing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "PROCESSING_ERROR"),
93 ApiError::Cache(_) => (StatusCode::INTERNAL_SERVER_ERROR, "CACHE_ERROR"),
94 ApiError::NotFound(_) => (StatusCode::NOT_FOUND, "NOT_FOUND"),
95 ApiError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"),
96 ApiError::Join(_) => (StatusCode::INTERNAL_SERVER_ERROR, "TASK_ERROR"),
97 ApiError::Parquet(_) => (StatusCode::INTERNAL_SERVER_ERROR, "PARQUET_ERROR"),
98 ApiError::Overloaded { .. } => (StatusCode::SERVICE_UNAVAILABLE, "OVERLOADED"),
99 };
100
101 let retry_after = match &self {
102 ApiError::Overloaded { retry_after_secs } => Some(*retry_after_secs),
103 _ => None,
104 };
105
106 let error = match &self {
107 ApiError::Cache(_) => {
109 tracing::error!(error = %self, "Cache store failure");
110 CACHE_CLIENT_MESSAGE.to_string()
111 }
112 _ => self.to_string(),
113 };
114 let mut response = error_response(status, code, error);
115 if let Some(secs) = retry_after {
116 if let Ok(v) = axum::http::HeaderValue::from_str(&secs.to_string()) {
117 response.headers_mut().insert(axum::http::header::RETRY_AFTER, v);
118 }
119 }
120 response
121 }
122}
123
124impl From<ifc_lite_core::Error> for ApiError {
125 fn from(err: ifc_lite_core::Error) -> Self {
126 ApiError::Processing(err.to_string())
127 }
128}
129
130impl From<ifc_lite_geometry::Error> for ApiError {
131 fn from(err: ifc_lite_geometry::Error) -> Self {
132 ApiError::Processing(err.to_string())
133 }
134}
135
136impl From<cacache::Error> for ApiError {
137 fn from(err: cacache::Error) -> Self {
138 ApiError::Cache(err.to_string())
139 }
140}
141
142impl From<serde_json::Error> for ApiError {
143 fn from(err: serde_json::Error) -> Self {
144 ApiError::Internal(format!("JSON error: {}", err))
145 }
146}
147
148impl From<crate::services::ParquetError> for ApiError {
149 fn from(err: crate::services::ParquetError) -> Self {
150 ApiError::Parquet(err.to_string())
151 }
152}
153
154impl From<crate::services::parquet_data_model::DataModelParquetError> for ApiError {
155 fn from(err: crate::services::parquet_data_model::DataModelParquetError) -> Self {
156 ApiError::Parquet(err.to_string())
157 }
158}
159
160#[cfg(test)]
161#[path = "error_tests.rs"]
162mod error_tests;