systemprompt_api/services/proxy/
errors.rs1use axum::body::Body;
7use axum::http::StatusCode;
8use axum::response::{IntoResponse, Response};
9use systemprompt_identifiers::error::IdValidationError;
10use systemprompt_mcp::McpDomainError;
11use systemprompt_models::api::ApiError;
12use systemprompt_traits::RegistryError;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum ProxyError {
17 #[error("Service '{service}' not found in inventory")]
18 ServiceNotFound { service: String },
19
20 #[error("Service '{service}' is not running (status: {status})")]
21 ServiceNotRunning { service: String, status: String },
22
23 #[error("Failed to connect to {service} at {url}: {source}")]
24 ConnectionFailed {
25 service: String,
26 url: String,
27 #[source]
28 source: reqwest::Error,
29 },
30
31 #[error("Request to {service} timed out")]
32 Timeout { service: String },
33
34 #[error("Invalid response from {service}")]
35 InvalidResponse {
36 service: String,
37 #[source]
38 source: ResponseBuildError,
39 },
40
41 #[error("Failed to build URL for {service}: {reason}")]
42 UrlConstructionFailed { service: String, reason: String },
43
44 #[error("Failed to read the request body")]
45 BodyExtractionFailed {
46 #[source]
47 source: axum::Error,
48 },
49
50 #[error("Invalid HTTP method '{method}'")]
51 InvalidMethod {
52 method: String,
53 #[source]
54 source: http::method::InvalidMethod,
55 },
56
57 #[error("Database error when looking up service '{service}': {source}")]
58 DatabaseError {
59 service: String,
60 #[source]
61 source: systemprompt_traits::RepositoryError,
62 },
63
64 #[error("Authentication required for service '{service}'")]
65 AuthenticationRequired { service: String },
66
67 #[error("OAuth challenge response")]
68 AuthChallenge(Box<Response<Body>>),
69
70 #[error("Access forbidden for service '{service}'")]
71 Forbidden { service: String },
72
73 #[error("Missing request context: {message}")]
74 MissingContext { message: String },
75
76 #[error("Service name '{service}' is not a valid agent name")]
77 InvalidServiceName {
78 service: String,
79 #[source]
80 source: IdValidationError,
81 },
82
83 #[error(transparent)]
84 InvalidIdentifier(#[from] IdValidationError),
85
86 #[error("Service registry lookup failed for '{service}'")]
87 RegistryLookupFailed {
88 service: String,
89 #[source]
90 source: RegistryError,
91 },
92
93 #[error("Service '{service}' could not be restarted")]
94 RestartFailed {
95 service: String,
96 #[source]
97 source: McpDomainError,
98 },
99
100 #[error("Connect your account for '{service}' before using this server")]
101 ProviderNotConnected { service: String },
102
103 #[error("External MCP server '{service}' could not be resolved")]
104 ExternalResolveFailed {
105 service: String,
106 #[source]
107 source: McpDomainError,
108 },
109
110 #[error("MCP registry could not be read while resolving '{service}': {source}")]
111 RegistryUnavailable {
112 service: String,
113 #[source]
114 source: McpDomainError,
115 },
116}
117
118#[derive(Debug, Error)]
119pub enum ResponseBuildError {
120 #[error("upstream returned an invalid HTTP status {status}")]
121 Status {
122 status: u16,
123 #[source]
124 source: http::status::InvalidStatusCode,
125 },
126 #[error("failed to read the upstream response body")]
127 Body(#[source] reqwest::Error),
128 #[error("failed to assemble the proxied response")]
129 Assemble(#[source] Box<http::Error>),
130}
131
132impl ProxyError {
133 pub fn to_status_code(&self) -> StatusCode {
134 match self {
135 Self::ServiceNotFound { .. } => StatusCode::NOT_FOUND,
136 Self::ServiceNotRunning { .. } | Self::RestartFailed { .. } => {
137 StatusCode::SERVICE_UNAVAILABLE
138 },
139 Self::ConnectionFailed { .. }
140 | Self::InvalidResponse { .. }
141 | Self::ExternalResolveFailed { .. } => StatusCode::BAD_GATEWAY,
142 Self::Timeout { .. } => StatusCode::GATEWAY_TIMEOUT,
143 Self::UrlConstructionFailed { .. }
144 | Self::DatabaseError { .. }
145 | Self::RegistryUnavailable { .. }
146 | Self::RegistryLookupFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
147 Self::BodyExtractionFailed { .. }
148 | Self::InvalidMethod { .. }
149 | Self::InvalidServiceName { .. }
150 | Self::InvalidIdentifier(_) => StatusCode::BAD_REQUEST,
151 Self::AuthenticationRequired { .. } | Self::MissingContext { .. } => {
152 StatusCode::UNAUTHORIZED
153 },
154 Self::AuthChallenge(response) => response.status(),
155 Self::Forbidden { .. } => StatusCode::FORBIDDEN,
156 Self::ProviderNotConnected { .. } => StatusCode::CONFLICT,
157 }
158 }
159
160 pub const fn error_key(&self) -> &'static str {
161 match self {
162 Self::ServiceNotFound { .. } => "service_not_found",
163 Self::ServiceNotRunning { .. } => "service_not_running",
164 Self::RestartFailed { .. } => "restart_failed",
165 Self::ConnectionFailed { .. } => "connection_failed",
166 Self::Timeout { .. } => "timeout",
167 Self::InvalidResponse { .. } => "invalid_response",
168 Self::UrlConstructionFailed { .. } => "url_construction_failed",
169 Self::BodyExtractionFailed { .. } => "body_extraction_failed",
170 Self::InvalidMethod { .. } => "invalid_method",
171 Self::DatabaseError { .. } => "database_error",
172 Self::AuthenticationRequired { .. } => "authentication_required",
173 Self::AuthChallenge(_) => "auth_challenge",
174 Self::Forbidden { .. } => "forbidden",
175 Self::MissingContext { .. } => "missing_context",
176 Self::InvalidServiceName { .. } => "invalid_service_name",
177 Self::InvalidIdentifier(_) => "invalid_identifier",
178 Self::RegistryUnavailable { .. } => "registry_unavailable",
179 Self::RegistryLookupFailed { .. } => "registry_lookup_failed",
180 Self::ProviderNotConnected { .. } => "provider_not_connected",
181 Self::ExternalResolveFailed { .. } => "external_resolve_failed",
182 }
183 }
184}
185
186impl From<ProxyError> for StatusCode {
187 fn from(error: ProxyError) -> Self {
188 error.to_status_code()
189 }
190}
191
192impl IntoResponse for ProxyError {
193 fn into_response(self) -> Response {
194 if let Self::AuthChallenge(response) = self {
195 return *response;
196 }
197 let status = self.to_status_code();
198 let error_key = self.error_key();
199 let api_error = match status {
200 StatusCode::NOT_FOUND => ApiError::not_found(self.to_string()),
201 StatusCode::UNAUTHORIZED => ApiError::unauthorized(self.to_string()),
202 StatusCode::FORBIDDEN => ApiError::forbidden(self.to_string()),
203 StatusCode::BAD_REQUEST => ApiError::bad_request(self.to_string()),
204 StatusCode::CONFLICT => ApiError::conflict(self.to_string()),
205 StatusCode::SERVICE_UNAVAILABLE
206 | StatusCode::BAD_GATEWAY
207 | StatusCode::GATEWAY_TIMEOUT => ApiError::service_unavailable("Proxy request failed")
208 .with_details(cause_chain(&self)),
209 _ => ApiError::internal_error("Proxy request failed").with_details(cause_chain(&self)),
210 };
211 api_error.with_error_key(error_key).into_response()
212 }
213}
214
215fn cause_chain(error: &dyn std::error::Error) -> String {
219 let mut chain = error.to_string();
220 let mut next = error.source();
221 while let Some(cause) = next {
222 chain.push_str(": ");
223 chain.push_str(&cause.to_string());
224 next = cause.source();
225 }
226 chain
227}