ifc_lite_server/
error.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Error types and handling for the server.
6
7use axum::{
8    http::StatusCode,
9    response::{IntoResponse, Response},
10    Json,
11};
12use serde::Serialize;
13use thiserror::Error;
14
15/// API error types.
16#[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
52/// The `error` text of a `CACHE_ERROR` response. The underlying detail stays
53/// in the server log: cacache's error text names the index bucket or content
54/// file it failed on, which is the absolute `CACHE_DIR` layout.
55const CACHE_CLIENT_MESSAGE: &str = "Cache error: the server's cache store failed";
56
57/// The one error body every route answers with (#5750): `{"error", "code"}`,
58/// `error` a human-readable message and `code` a stable `SCREAMING_SNAKE`
59/// identifier a client can branch on. [`ApiError`] renders it for every
60/// handler failure, and `middleware::error_envelope` renders it for the
61/// responses no handler writes (extractor rejections, unknown routes, wrong
62/// methods, timeouts, panics), so a client decodes one shape for every
63/// non-2xx status. Documented in `docs/guide/server.md` ("Error envelope").
64#[derive(Debug, Serialize)]
65pub struct ErrorResponse {
66    pub error: String,
67    pub code: String,
68}
69
70/// Render the shared envelope. The one builder of an error body: `ApiError`
71/// goes through it, and so do the responses written outside a handler (the
72/// bearer layer's `401`, `middleware::error_envelope`).
73pub 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            // Logged inside the request's trace span, never sent.
108            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;