bge_m3_embedding_server/
error.rs1use axum::{
18 http::StatusCode,
19 response::{IntoResponse, Response},
20 Json,
21};
22use serde_json::json;
23use tracing::error;
24
25#[derive(Debug)]
27pub enum AppError {
28 InvalidRequest(String),
31 ServiceUnavailable(String),
34 Internal(String),
37}
38
39impl IntoResponse for AppError {
40 fn into_response(self) -> Response {
41 let (status, error_type, code, message) = match self {
42 AppError::InvalidRequest(msg) => (
43 StatusCode::BAD_REQUEST,
44 "invalid_request_error",
45 400u16,
46 msg,
47 ),
48 AppError::ServiceUnavailable(msg) => (
49 StatusCode::SERVICE_UNAVAILABLE,
50 "service_unavailable",
51 503u16,
52 msg,
53 ),
54 AppError::Internal(msg) => (
55 StatusCode::INTERNAL_SERVER_ERROR,
56 "internal_error",
57 500u16,
58 msg,
59 ),
60 };
61
62 let body = json!({
63 "error": {
64 "message": message,
65 "type": error_type,
66 "code": code
67 }
68 });
69
70 (status, Json(body)).into_response()
71 }
72}
73
74impl From<anyhow::Error> for AppError {
75 fn from(err: anyhow::Error) -> Self {
76 error!(error = %err, "Internal error");
77 AppError::Internal("internal server error".to_string())
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use axum::body::to_bytes;
85 use axum::response::IntoResponse;
86
87 async fn response_parts(err: AppError) -> (StatusCode, serde_json::Value) {
88 let response = err.into_response();
89 let status = response.status();
90 let bytes = to_bytes(response.into_body(), usize::MAX)
91 .await
92 .expect("failed to read body");
93 let body: serde_json::Value =
94 serde_json::from_slice(&bytes).expect("body is not valid JSON");
95 (status, body)
96 }
97
98 #[tokio::test]
99 async fn invalid_request_serializes_as_400() {
100 let (status, body) =
101 response_parts(AppError::InvalidRequest("bad input".to_string())).await;
102 assert_eq!(status, StatusCode::BAD_REQUEST);
103 assert_eq!(body["error"]["code"], 400);
104 assert_eq!(body["error"]["type"], "invalid_request_error");
105 assert_eq!(body["error"]["message"], "bad input");
106 }
107
108 #[tokio::test]
109 async fn service_unavailable_serializes_as_503() {
110 let (status, body) =
111 response_parts(AppError::ServiceUnavailable("model not ready".to_string())).await;
112 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
113 assert_eq!(body["error"]["code"], 503);
114 assert_eq!(body["error"]["type"], "service_unavailable");
115 assert_eq!(body["error"]["message"], "model not ready");
116 }
117
118 #[tokio::test]
119 async fn internal_error_serializes_as_500() {
120 let (status, body) =
121 response_parts(AppError::Internal("unexpected failure".to_string())).await;
122 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
123 assert_eq!(body["error"]["code"], 500);
124 assert_eq!(body["error"]["type"], "internal_error");
125 assert_eq!(body["error"]["message"], "unexpected failure");
126 }
127
128 #[tokio::test]
129 async fn from_anyhow_error_produces_generic_message() {
130 let err = anyhow::anyhow!("secret path /var/models/onnx failed to load");
131 let app_err: AppError = err.into();
132 let (status, body) = response_parts(app_err).await;
133 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
134 assert_eq!(
135 body["error"]["message"], "internal server error",
136 "internal details must not leak to client"
137 );
138 }
139}