systemprompt_api/services/proxy/
errors.rs1use 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}