Skip to main content

kagi_server/server/
errors.rs

1use axum::Json;
2use axum::http::StatusCode;
3use axum::response::{IntoResponse, Response};
4use serde_json::json;
5
6#[derive(Debug)]
7pub enum ServerError {
8    BadRequest(String),
9    BadEnvelope(String),
10    DecryptFailed(String),
11    AuthFailed,
12    Forbidden,
13    NotFound,
14    Conflict {
15        code: String,
16        message: String,
17        details: Option<serde_json::Value>,
18    },
19    InvalidPath(String),
20    InvalidRevision,
21    InvalidProjectState(String),
22    ServerKeyMismatch,
23    PayloadTooLarge,
24    Internal(String),
25}
26
27impl IntoResponse for ServerError {
28    fn into_response(self) -> Response {
29        let (status, code, message, details): (
30            StatusCode,
31            &str,
32            String,
33            Option<serde_json::Value>,
34        ) = match &self {
35            ServerError::BadRequest(msg) => {
36                (StatusCode::BAD_REQUEST, "bad_request", msg.clone(), None)
37            }
38            ServerError::BadEnvelope(msg) => {
39                (StatusCode::BAD_REQUEST, "bad_envelope", msg.clone(), None)
40            }
41            ServerError::DecryptFailed(msg) => {
42                (StatusCode::BAD_REQUEST, "decrypt_failed", msg.clone(), None)
43            }
44            ServerError::AuthFailed => (
45                StatusCode::UNAUTHORIZED,
46                "auth_failed",
47                "authentication failed".into(),
48                None,
49            ),
50            ServerError::Forbidden => (
51                StatusCode::FORBIDDEN,
52                "forbidden",
53                "insufficient capabilities".into(),
54                None,
55            ),
56            ServerError::NotFound => (
57                StatusCode::NOT_FOUND,
58                "not_found",
59                "resource not found".into(),
60                None,
61            ),
62            ServerError::Conflict {
63                code,
64                message,
65                details,
66            } => {
67                return (StatusCode::CONFLICT, Json(json!({"ok": false, "error": {"code": code, "message": message, "details": details}}))).into_response();
68            }
69            ServerError::InvalidPath(msg) => {
70                (StatusCode::BAD_REQUEST, "invalid_path", msg.clone(), None)
71            }
72            ServerError::InvalidRevision => (
73                StatusCode::BAD_REQUEST,
74                "invalid_revision",
75                "revision mismatch".into(),
76                None,
77            ),
78            ServerError::InvalidProjectState(msg) => (
79                StatusCode::BAD_REQUEST,
80                "invalid_project_state",
81                msg.clone(),
82                None,
83            ),
84            ServerError::ServerKeyMismatch => (
85                StatusCode::BAD_REQUEST,
86                "server_key_mismatch",
87                "unknown server key".into(),
88                None,
89            ),
90            ServerError::PayloadTooLarge => (
91                StatusCode::PAYLOAD_TOO_LARGE,
92                "payload_too_large",
93                "project storage limit exceeded".into(),
94                None,
95            ),
96            ServerError::Internal(msg) => (
97                StatusCode::INTERNAL_SERVER_ERROR,
98                "internal",
99                msg.clone(),
100                None,
101            ),
102        };
103
104        let body = Json(json!({
105            "ok": false,
106            "error": { "code": code, "message": message, "details": details }
107        }));
108        (status, body).into_response()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use axum::response::IntoResponse;
116
117    fn status_of(err: ServerError) -> StatusCode {
118        err.into_response().status()
119    }
120
121    #[test]
122    fn test_bad_request_status() {
123        assert_eq!(
124            status_of(ServerError::BadRequest("x".into())),
125            StatusCode::BAD_REQUEST
126        );
127    }
128
129    #[test]
130    fn test_auth_failed_status() {
131        assert_eq!(status_of(ServerError::AuthFailed), StatusCode::UNAUTHORIZED);
132    }
133
134    #[test]
135    fn test_forbidden_status() {
136        assert_eq!(status_of(ServerError::Forbidden), StatusCode::FORBIDDEN);
137    }
138
139    #[test]
140    fn test_not_found_status() {
141        assert_eq!(status_of(ServerError::NotFound), StatusCode::NOT_FOUND);
142    }
143
144    #[test]
145    fn test_conflict_status() {
146        let err = ServerError::Conflict {
147            code: "c".into(),
148            message: "m".into(),
149            details: None,
150        };
151        assert_eq!(status_of(err), StatusCode::CONFLICT);
152    }
153
154    #[test]
155    fn test_internal_status() {
156        assert_eq!(
157            status_of(ServerError::Internal("x".into())),
158            StatusCode::INTERNAL_SERVER_ERROR
159        );
160    }
161
162    #[test]
163    fn test_payload_too_large_status() {
164        assert_eq!(
165            status_of(ServerError::PayloadTooLarge),
166            StatusCode::PAYLOAD_TOO_LARGE
167        );
168    }
169}