Skip to main content

af_web/
error.rs

1//! JSON error envelope. Port of the reusable shape of `agent_core/api/errors.py`.
2//!
3//! Every handler returns `Result<T, ApiError>`; the error renders as a stable
4//! JSON body `{ "error": "...", "request_id": "..." }` with the right status.
5//! Carrying the request id back to the client is the Error-Localization rule on
6//! the wire — a user-reported failure is greppable by id.
7
8use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use axum::Json;
11use serde_json::json;
12
13/// A handler error with an HTTP status and a user-safe message.
14#[derive(Debug, Clone)]
15pub struct ApiError {
16    /// HTTP status.
17    pub status: StatusCode,
18    /// Human-readable message.
19    pub message: String,
20    /// Transport request identity for tracing and idempotency.
21    pub request_id: Option<String>,
22}
23
24impl ApiError {
25    /// An error envelope with an explicit status.
26    pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
27        Self {
28            status,
29            message: message.into(),
30            request_id: None,
31        }
32    }
33
34    /// HTTP 400.
35    pub fn bad_request(message: impl Into<String>) -> Self {
36        Self::new(StatusCode::BAD_REQUEST, message)
37    }
38    /// HTTP 404.
39    pub fn not_found(message: impl Into<String>) -> Self {
40        Self::new(StatusCode::NOT_FOUND, message)
41    }
42    /// HTTP 500.
43    pub fn internal(message: impl Into<String>) -> Self {
44        Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
45    }
46
47    /// Attach the request id (the latency/request-id middleware sets one).
48    pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
49        self.request_id = Some(id.into());
50        self
51    }
52}
53
54impl IntoResponse for ApiError {
55    fn into_response(self) -> Response {
56        let body = json!({
57            "error": self.message,
58            "request_id": self.request_id,
59        });
60        (self.status, Json(body)).into_response()
61    }
62}