Skip to main content

linkrs_server/http/
error.rs

1use axum::{
2    http::StatusCode,
3    response::{IntoResponse, Response},
4    Json,
5};
6use serde_json::json;
7use std::fmt;
8
9#[derive(Debug)]
10pub enum HttpError {
11    BadRequest(String),
12    Unauthorized(String),
13    Forbidden(String),
14    NotFound(String),
15    Conflict(String),
16    InternalError(String),
17}
18
19impl fmt::Display for HttpError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            HttpError::BadRequest(msg) => write!(f, "Bad Request: {}", msg),
23            HttpError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
24            HttpError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
25            HttpError::NotFound(msg) => write!(f, "Not Found: {}", msg),
26            HttpError::Conflict(msg) => write!(f, "Conflict: {}", msg),
27            HttpError::InternalError(msg) => write!(f, "Internal Error: {}", msg),
28        }
29    }
30}
31
32impl std::error::Error for HttpError {}
33
34impl IntoResponse for HttpError {
35    fn into_response(self) -> Response {
36        let (status, message) = match self {
37            HttpError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
38            HttpError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg),
39            HttpError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
40            HttpError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
41            HttpError::Conflict(msg) => (StatusCode::CONFLICT, msg),
42            HttpError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
43        };
44
45        let body = Json(json!({
46            "error": message,
47            "status": status.as_u16(),
48        }));
49
50        (status, body).into_response()
51    }
52}
53
54impl HttpError {
55    /// Generate a BadRequest error.
56    pub fn bad_request<T: Into<String>>(msg: T) -> Self {
57        HttpError::BadRequest(msg.into())
58    }
59
60    /// Generate a “NotFound” error.
61    pub fn not_found<T: Into<String>>(msg: T) -> Self {
62        HttpError::NotFound(msg.into())
63    }
64
65    /// An “Unauthorized” error was generated.
66    pub fn unauthorized<T: Into<String>>(msg: T) -> Self {
67        HttpError::Unauthorized(msg.into())
68    }
69
70    /// Generate a Forbidden error for authenticated users lacking permission.
71    pub fn forbidden<T: Into<String>>(msg: T) -> Self {
72        HttpError::Forbidden(msg.into())
73    }
74
75    /// Generate an InternalError.
76    pub fn internal<T: Into<String>>(msg: T) -> Self {
77        HttpError::InternalError(msg.into())
78    }
79
80    /// Classify the stable transaction error code emitted by the transaction
81    /// crate instead of collapsing every operation into HTTP 500.
82    pub fn transaction_message<T: Into<String>>(message: T) -> Self {
83        let message = message.into();
84        if message.contains("[transaction_not_found]") {
85            Self::NotFound(message)
86        } else if message.contains("[transaction_not_owner]") {
87            Self::Unauthorized(message)
88        } else if message.contains("[write_transaction_conflict]") {
89            Self::Conflict(message)
90        } else if message.contains("[transaction_timeout]")
91            || message.contains("[transaction_expired]")
92        {
93            Self::BadRequest(message)
94        } else if message.contains("[invalid_state") || message.contains("[savepoint") {
95            Self::Conflict(message)
96        } else {
97            Self::InternalError(message)
98        }
99    }
100}
101
102impl From<linkrs_api::api_core::CoreError> for HttpError {
103    fn from(err: linkrs_api::api_core::CoreError) -> Self {
104        use linkrs_api::api_core::CoreError;
105        match err {
106            CoreError::NotFound(msg) => HttpError::NotFound(msg),
107            CoreError::InvalidParameter(msg) => HttpError::BadRequest(msg),
108            CoreError::QueryExecutionFailed(msg) => HttpError::InternalError(msg),
109            CoreError::TransactionFailed(msg) => HttpError::transaction_message(msg),
110            CoreError::SchemaOperationFailed(msg) => HttpError::InternalError(msg),
111            CoreError::StorageError(msg) => HttpError::InternalError(msg),
112            CoreError::Internal(msg) => HttpError::InternalError(msg),
113            CoreError::DetailedQueryError { message, .. } => HttpError::InternalError(message),
114            CoreError::SyncError(msg) => HttpError::InternalError(msg),
115            CoreError::VectorError(msg) => HttpError::InternalError(msg),
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::HttpError;
123
124    #[test]
125    fn transaction_error_codes_survive_handler_context() {
126        assert!(matches!(
127            HttpError::transaction_message("Failed to commit: [transaction_not_owner] denied"),
128            HttpError::Unauthorized(_)
129        ));
130        assert!(matches!(
131            HttpError::transaction_message("Failed to commit: [write_transaction_conflict]"),
132            HttpError::Conflict(_)
133        ));
134        assert!(matches!(
135            HttpError::transaction_message("Failed to commit: [transaction_timeout]"),
136            HttpError::BadRequest(_)
137        ));
138    }
139}