1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use lago_core::LagoError;
4use serde::Serialize;
5
6#[derive(Debug)]
8pub enum ApiError {
9 Lago(LagoError),
11 BadRequest(String),
13 NotFound(String),
15 Internal(String),
17}
18
19#[derive(Serialize)]
21struct ErrorBody {
22 error: String,
23 message: String,
24}
25
26impl IntoResponse for ApiError {
27 fn into_response(self) -> Response {
28 let (status, error_type, message) = match &self {
29 ApiError::Lago(e) => match e {
30 LagoError::SessionNotFound(id) => (
31 StatusCode::NOT_FOUND,
32 "session_not_found",
33 format!("session not found: {id}"),
34 ),
35 LagoError::BranchNotFound(id) => (
36 StatusCode::NOT_FOUND,
37 "branch_not_found",
38 format!("branch not found: {id}"),
39 ),
40 LagoError::EventNotFound(id) => (
41 StatusCode::NOT_FOUND,
42 "event_not_found",
43 format!("event not found: {id}"),
44 ),
45 LagoError::BlobNotFound(hash) => (
46 StatusCode::NOT_FOUND,
47 "blob_not_found",
48 format!("blob not found: {hash}"),
49 ),
50 LagoError::FileNotFound(path) => (
51 StatusCode::NOT_FOUND,
52 "file_not_found",
53 format!("file not found: {path}"),
54 ),
55 LagoError::InvalidArgument(msg) => {
56 (StatusCode::BAD_REQUEST, "invalid_argument", msg.clone())
57 }
58 LagoError::SequenceConflict { expected, actual } => (
59 StatusCode::CONFLICT,
60 "sequence_conflict",
61 format!("sequence conflict: expected {expected}, got {actual}"),
62 ),
63 LagoError::PolicyDenied(msg) => {
64 (StatusCode::FORBIDDEN, "policy_denied", msg.clone())
65 }
66 LagoError::Serialization(e) => (
67 StatusCode::BAD_REQUEST,
68 "serialization_error",
69 format!("serialization error: {e}"),
70 ),
71 LagoError::HashLine(e) => (
72 StatusCode::BAD_REQUEST,
73 "hashline_error",
74 format!("hashline error: {e}"),
75 ),
76 LagoError::Sandbox(msg) => (
77 StatusCode::BAD_REQUEST,
78 "sandbox_error",
79 format!("sandbox error: {msg}"),
80 ),
81 _ => (
82 StatusCode::INTERNAL_SERVER_ERROR,
83 "internal_error",
84 format!("internal error: {e}"),
85 ),
86 },
87 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg.clone()),
88 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg.clone()),
89 ApiError::Internal(msg) => (
90 StatusCode::INTERNAL_SERVER_ERROR,
91 "internal_error",
92 msg.clone(),
93 ),
94 };
95
96 let body = ErrorBody {
97 error: error_type.to_string(),
98 message,
99 };
100
101 (status, axum::Json(body)).into_response()
102 }
103}
104
105impl From<LagoError> for ApiError {
106 fn from(e: LagoError) -> Self {
107 ApiError::Lago(e)
108 }
109}
110
111impl From<serde_json::Error> for ApiError {
112 fn from(e: serde_json::Error) -> Self {
113 ApiError::BadRequest(format!("invalid JSON: {e}"))
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use axum::response::IntoResponse;
121
122 #[test]
123 fn api_error_from_lago_session_not_found() {
124 let e: ApiError = LagoError::SessionNotFound("S1".into()).into();
125 let resp = e.into_response();
126 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
127 }
128
129 #[test]
130 fn api_error_from_lago_branch_not_found() {
131 let e: ApiError = LagoError::BranchNotFound("B1".into()).into();
132 let resp = e.into_response();
133 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
134 }
135
136 #[test]
137 fn api_error_from_lago_policy_denied() {
138 let e: ApiError = LagoError::PolicyDenied("blocked".into()).into();
139 let resp = e.into_response();
140 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
141 }
142
143 #[test]
144 fn api_error_from_lago_sequence_conflict() {
145 let e: ApiError = LagoError::SequenceConflict {
146 expected: 5,
147 actual: 3,
148 }
149 .into();
150 let resp = e.into_response();
151 assert_eq!(resp.status(), StatusCode::CONFLICT);
152 }
153
154 #[test]
155 fn api_error_from_lago_invalid_argument() {
156 let e: ApiError = LagoError::InvalidArgument("bad input".into()).into();
157 let resp = e.into_response();
158 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
159 }
160
161 #[test]
162 fn api_error_from_lago_internal() {
163 let e: ApiError = LagoError::Journal("disk error".into()).into();
164 let resp = e.into_response();
165 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
166 }
167
168 #[test]
169 fn api_error_bad_request() {
170 let e = ApiError::BadRequest("bad".into());
171 let resp = e.into_response();
172 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
173 }
174
175 #[test]
176 fn api_error_not_found() {
177 let e = ApiError::NotFound("missing".into());
178 let resp = e.into_response();
179 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
180 }
181
182 #[test]
183 fn api_error_internal() {
184 let e = ApiError::Internal("oops".into());
185 let resp = e.into_response();
186 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
187 }
188
189 #[test]
190 fn api_error_from_serde_json() {
191 let err: Result<serde_json::Value, _> = serde_json::from_str("{bad");
192 let e: ApiError = err.unwrap_err().into();
193 match e {
194 ApiError::BadRequest(msg) => assert!(msg.contains("invalid JSON")),
195 _ => panic!("expected BadRequest"),
196 }
197 }
198
199 #[test]
200 fn api_error_from_lago_hashline() {
201 let hl_err = lago_core::hashline::HashLineError::LineOutOfBounds {
202 line_num: 10,
203 total_lines: 5,
204 };
205 let e: ApiError = LagoError::HashLine(hl_err).into();
206 let resp = e.into_response();
207 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
208 }
209
210 #[test]
211 fn api_error_from_lago_sandbox() {
212 let e: ApiError = LagoError::Sandbox("container failed".into()).into();
213 let resp = e.into_response();
214 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
215 }
216}