Skip to main content

bge_m3_embedding_server/
error.rs

1// Copyright (c) 2026 J. Patrick Fulton
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Application-level error types that map to HTTP status codes.
16
17use axum::{
18    http::StatusCode,
19    response::{IntoResponse, Response},
20    Json,
21};
22use serde_json::json;
23use tracing::error;
24
25/// Application-level errors that map to HTTP status codes.
26#[derive(Debug)]
27pub enum AppError {
28    /// The request was malformed or violates input constraints.
29    /// Maps to HTTP 400 Bad Request.
30    InvalidRequest(String),
31    /// The service is not yet ready (model loading) or has no live workers.
32    /// Maps to HTTP 503 Service Unavailable.
33    ServiceUnavailable(String),
34    /// An unexpected internal error occurred during embedding.
35    /// Maps to HTTP 500 Internal Server Error.
36    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}