Skip to main content

systemprompt_api/services/proxy/
errors.rs

1//! Proxy error types and their HTTP status mapping.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use axum::body::Body;
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use systemprompt_models::api::{ApiError, ErrorCode};
10use thiserror::Error;
11
12#[derive(Debug, Error)]
13pub enum ProxyError {
14    #[error("Service '{service}' not found in inventory")]
15    ServiceNotFound { service: String },
16
17    #[error("Service '{service}' is not running (status: {status})")]
18    ServiceNotRunning { service: String, status: String },
19
20    #[error("Failed to connect to {service} at {url}: {source}")]
21    ConnectionFailed {
22        service: String,
23        url: String,
24        #[source]
25        source: reqwest::Error,
26    },
27
28    #[error("Request to {service} timed out")]
29    Timeout { service: String },
30
31    #[error("Invalid response from {service}: {reason}")]
32    InvalidResponse { service: String, reason: String },
33
34    #[error("Failed to build URL for {service}: {reason}")]
35    UrlConstructionFailed { service: String, reason: String },
36
37    #[error("Failed to extract request body: {source}")]
38    BodyExtractionFailed {
39        #[source]
40        source: axum::Error,
41    },
42
43    #[error("Invalid HTTP method: {reason}")]
44    InvalidMethod { reason: String },
45
46    #[error("Database error when looking up service '{service}': {source}")]
47    DatabaseError {
48        service: String,
49        #[source]
50        source: systemprompt_database::RepositoryError,
51    },
52
53    #[error("Authentication required for service '{service}'")]
54    AuthenticationRequired { service: String },
55
56    #[error("OAuth challenge response")]
57    AuthChallenge(Box<Response<Body>>),
58
59    #[error("Access forbidden for service '{service}'")]
60    Forbidden { service: String },
61
62    #[error("Missing request context: {message}")]
63    MissingContext { message: String },
64
65    #[error("Service name '{service}' is not a valid agent name: {reason}")]
66    InvalidServiceName { service: String, reason: String },
67}
68
69impl ProxyError {
70    pub fn to_status_code(&self) -> StatusCode {
71        match self {
72            Self::ServiceNotFound { .. } => StatusCode::NOT_FOUND,
73            Self::ServiceNotRunning { .. } => StatusCode::SERVICE_UNAVAILABLE,
74            Self::ConnectionFailed { .. } | Self::InvalidResponse { .. } => StatusCode::BAD_GATEWAY,
75            Self::Timeout { .. } => StatusCode::GATEWAY_TIMEOUT,
76            Self::UrlConstructionFailed { .. } | Self::DatabaseError { .. } => {
77                StatusCode::INTERNAL_SERVER_ERROR
78            },
79            Self::BodyExtractionFailed { .. }
80            | Self::InvalidMethod { .. }
81            | Self::InvalidServiceName { .. } => StatusCode::BAD_REQUEST,
82            Self::AuthenticationRequired { .. } | Self::MissingContext { .. } => {
83                StatusCode::UNAUTHORIZED
84            },
85            Self::AuthChallenge(response) => response.status(),
86            Self::Forbidden { .. } => StatusCode::FORBIDDEN,
87        }
88    }
89}
90
91impl From<ProxyError> for StatusCode {
92    fn from(error: ProxyError) -> Self {
93        error.to_status_code()
94    }
95}
96
97impl IntoResponse for ProxyError {
98    fn into_response(self) -> Response {
99        match self {
100            Self::AuthChallenge(response) => (*response).into_response(),
101            ref error => {
102                let status = error.to_status_code();
103                let error_type = match &self {
104                    Self::ServiceNotFound { .. } => "service_not_found",
105                    Self::ServiceNotRunning { .. } => "service_not_running",
106                    Self::ConnectionFailed { .. } => "connection_failed",
107                    Self::Timeout { .. } => "timeout",
108                    Self::InvalidResponse { .. } => "invalid_response",
109                    Self::UrlConstructionFailed { .. } => "url_construction_failed",
110                    Self::BodyExtractionFailed { .. } => "body_extraction_failed",
111                    Self::InvalidMethod { .. } => "invalid_method",
112                    Self::DatabaseError { .. } => "database_error",
113                    Self::AuthenticationRequired { .. } => "authentication_required",
114                    Self::AuthChallenge(_) => "auth_challenge",
115                    Self::Forbidden { .. } => "forbidden",
116                    Self::MissingContext { .. } => "missing_context",
117                    Self::InvalidServiceName { .. } => "invalid_service_name",
118                };
119
120                if status.is_server_error() {
121                    tracing::error!(
122                        error_type = %error_type,
123                        status_code = %status.as_u16(),
124                        error = %self,
125                        "Proxy server error"
126                    );
127                } else if status.is_client_error() {
128                    tracing::warn!(
129                        error_type = %error_type,
130                        status_code = %status.as_u16(),
131                        error = %self,
132                        "Proxy client error"
133                    );
134                }
135
136                let message = self.to_string();
137                let api_error = match status {
138                    StatusCode::NOT_FOUND => ApiError::not_found(message),
139                    StatusCode::UNAUTHORIZED => ApiError::unauthorized(message),
140                    StatusCode::FORBIDDEN => ApiError::forbidden(message),
141                    StatusCode::BAD_REQUEST => ApiError::bad_request(message),
142                    StatusCode::SERVICE_UNAVAILABLE
143                    | StatusCode::BAD_GATEWAY
144                    | StatusCode::GATEWAY_TIMEOUT => {
145                        ApiError::new(ErrorCode::ServiceUnavailable, message)
146                    },
147                    _ => ApiError::internal_error(message),
148                };
149                api_error.into_response()
150            },
151        }
152    }
153}