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    #[error("MCP registry could not be read while resolving '{service}': {source}")]
69    RegistryUnavailable {
70        service: String,
71        #[source]
72        source: systemprompt_mcp::McpDomainError,
73    },
74}
75
76impl ProxyError {
77    pub fn to_status_code(&self) -> StatusCode {
78        match self {
79            Self::ServiceNotFound { .. } => StatusCode::NOT_FOUND,
80            Self::ServiceNotRunning { .. } => StatusCode::SERVICE_UNAVAILABLE,
81            Self::ConnectionFailed { .. } | Self::InvalidResponse { .. } => StatusCode::BAD_GATEWAY,
82            Self::Timeout { .. } => StatusCode::GATEWAY_TIMEOUT,
83            Self::UrlConstructionFailed { .. }
84            | Self::DatabaseError { .. }
85            | Self::RegistryUnavailable { .. } => StatusCode::INTERNAL_SERVER_ERROR,
86            Self::BodyExtractionFailed { .. }
87            | Self::InvalidMethod { .. }
88            | Self::InvalidServiceName { .. } => StatusCode::BAD_REQUEST,
89            Self::AuthenticationRequired { .. } | Self::MissingContext { .. } => {
90                StatusCode::UNAUTHORIZED
91            },
92            Self::AuthChallenge(response) => response.status(),
93            Self::Forbidden { .. } => StatusCode::FORBIDDEN,
94        }
95    }
96}
97
98impl From<ProxyError> for StatusCode {
99    fn from(error: ProxyError) -> Self {
100        error.to_status_code()
101    }
102}
103
104impl IntoResponse for ProxyError {
105    fn into_response(self) -> Response {
106        match self {
107            Self::AuthChallenge(response) => (*response).into_response(),
108            ref error => {
109                let status = error.to_status_code();
110                let error_type = match &self {
111                    Self::ServiceNotFound { .. } => "service_not_found",
112                    Self::ServiceNotRunning { .. } => "service_not_running",
113                    Self::ConnectionFailed { .. } => "connection_failed",
114                    Self::Timeout { .. } => "timeout",
115                    Self::InvalidResponse { .. } => "invalid_response",
116                    Self::UrlConstructionFailed { .. } => "url_construction_failed",
117                    Self::BodyExtractionFailed { .. } => "body_extraction_failed",
118                    Self::InvalidMethod { .. } => "invalid_method",
119                    Self::DatabaseError { .. } => "database_error",
120                    Self::AuthenticationRequired { .. } => "authentication_required",
121                    Self::AuthChallenge(_) => "auth_challenge",
122                    Self::Forbidden { .. } => "forbidden",
123                    Self::MissingContext { .. } => "missing_context",
124                    Self::InvalidServiceName { .. } => "invalid_service_name",
125                    Self::RegistryUnavailable { .. } => "registry_unavailable",
126                };
127
128                if status.is_server_error() {
129                    tracing::error!(
130                        error_type = %error_type,
131                        status_code = %status.as_u16(),
132                        error = %self,
133                        "Proxy server error"
134                    );
135                } else if status.is_client_error() {
136                    tracing::warn!(
137                        error_type = %error_type,
138                        status_code = %status.as_u16(),
139                        error = %self,
140                        "Proxy client error"
141                    );
142                }
143
144                let message = self.to_string();
145                let api_error = match status {
146                    StatusCode::NOT_FOUND => ApiError::not_found(message),
147                    StatusCode::UNAUTHORIZED => ApiError::unauthorized(message),
148                    StatusCode::FORBIDDEN => ApiError::forbidden(message),
149                    StatusCode::BAD_REQUEST => ApiError::bad_request(message),
150                    StatusCode::SERVICE_UNAVAILABLE
151                    | StatusCode::BAD_GATEWAY
152                    | StatusCode::GATEWAY_TIMEOUT => {
153                        ApiError::new(ErrorCode::ServiceUnavailable, message)
154                    },
155                    _ => ApiError::internal_error(message),
156                };
157                api_error.into_response()
158            },
159        }
160    }
161}