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
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}