Skip to main content

systemprompt_models/api/errors/
response.rs

1//! HTTP status mapping and the axum response for [`ApiError`].
2//!
3//! The response logs once by status class with the internal cause chain as a
4//! structured field; the body is the redacting wire shape.
5//!
6//! Copyright (c) systemprompt.io — Business Source License 1.1.
7//! See <https://systemprompt.io> for licensing details.
8
9use std::error::Error;
10
11use axum::Json;
12use axum::http::{StatusCode, header};
13use axum::response::IntoResponse;
14
15use super::{ApiError, ErrorCode};
16
17impl ErrorCode {
18    #[must_use]
19    pub const fn status_code(&self) -> StatusCode {
20        match self {
21            Self::NotFound => StatusCode::NOT_FOUND,
22            Self::BadRequest => StatusCode::BAD_REQUEST,
23            Self::Unauthorized => StatusCode::UNAUTHORIZED,
24            Self::Forbidden => StatusCode::FORBIDDEN,
25            Self::ValidationError => StatusCode::UNPROCESSABLE_ENTITY,
26            Self::ConflictError => StatusCode::CONFLICT,
27            Self::RateLimited => StatusCode::TOO_MANY_REQUESTS,
28            Self::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
29            Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
30        }
31    }
32}
33
34fn cause_chain(source: Option<&(dyn Error + Send + Sync + 'static)>) -> Option<String> {
35    let source = source?;
36    let mut chain = source.to_string();
37    let mut next = source.source();
38    while let Some(cause) = next {
39        chain.push_str(": ");
40        chain.push_str(&cause.to_string());
41        next = cause.source();
42    }
43    Some(chain)
44}
45
46impl IntoResponse for ApiError {
47    fn into_response(self) -> axum::response::Response {
48        let status = self.code.status_code();
49        let cause = cause_chain(self.source());
50
51        if status.is_server_error() {
52            tracing::error!(
53                error_code = ?self.code,
54                context = %self.message,
55                details = ?self.details,
56                error_key = ?self.error_key,
57                cause = ?cause,
58                path = ?self.path,
59                trace_id = ?self.trace_id,
60                "API server error response"
61            );
62        } else if status.is_client_error() {
63            tracing::warn!(
64                error_code = ?self.code,
65                message = %self.message,
66                error_key = ?self.error_key,
67                cause = ?cause,
68                path = ?self.path,
69                trace_id = ?self.trace_id,
70                "API client error response"
71            );
72        }
73
74        let mut response = (status, Json(self)).into_response();
75
76        if status == StatusCode::UNAUTHORIZED
77            && let Ok(header_value) =
78                "Bearer resource_metadata=\"/.well-known/oauth-protected-resource\"".parse()
79        {
80            response
81                .headers_mut()
82                .insert(header::WWW_AUTHENTICATE, header_value);
83        }
84
85        response
86    }
87}