Skip to main content

systemprompt_agent/services/a2a_server/errors/
jsonrpc.rs

1//! JSON-RPC 2.0 error envelope construction.
2//!
3//! [`JsonRpcErrorBuilder`] assembles spec-coded error responses with optional
4//! data payloads and structured logging; [`unauthorized_response`] and
5//! [`forbidden_response`] are the auth-failure shortcuts, and
6//! [`classify_database_error`] maps repository errors to user-facing messages.
7
8use crate::models::a2a::jsonrpc::NumberOrString;
9use axum::http::StatusCode;
10use serde_json::{Value, json};
11use systemprompt_logging::LogLevel;
12use systemprompt_traits::RepositoryError;
13
14pub fn classify_database_error(error: &RepositoryError) -> String {
15    let error_str = error.to_string();
16
17    if error_str.contains("FOREIGN KEY constraint failed") {
18        format!(
19            "Database constraint error: Referenced entity does not exist - {}",
20            error
21        )
22    } else if error_str.contains("UNIQUE constraint failed") {
23        format!("Database constraint error: Duplicate entry - {error}")
24    } else if error_str.contains("NOT NULL constraint failed") {
25        format!(
26            "Database constraint error: Required field missing - {}",
27            error
28        )
29    } else {
30        format!("Database error: {error}")
31    }
32}
33
34#[derive(Debug)]
35pub struct JsonRpcErrorBuilder {
36    code: i32,
37    message: String,
38    data: Option<Value>,
39    log_message: Option<String>,
40    log_level: LogLevel,
41}
42
43impl JsonRpcErrorBuilder {
44    pub fn new(code: i32, message: impl Into<String>) -> Self {
45        Self {
46            code,
47            message: message.into(),
48            data: None,
49            log_message: None,
50            log_level: LogLevel::Error,
51        }
52    }
53
54    pub fn with_data(mut self, data: Value) -> Self {
55        self.data = Some(data);
56        self
57    }
58
59    pub fn with_log(mut self, message: impl Into<String>, level: LogLevel) -> Self {
60        self.log_message = Some(message.into());
61        self.log_level = level;
62        self
63    }
64
65    pub fn log_error(mut self, message: impl Into<String>) -> Self {
66        self.log_message = Some(message.into());
67        self.log_level = LogLevel::Error;
68        self
69    }
70
71    pub fn log_warn(mut self, message: impl Into<String>) -> Self {
72        self.log_message = Some(message.into());
73        self.log_level = LogLevel::Warn;
74        self
75    }
76
77    pub fn build(self, request_id: &NumberOrString) -> Value {
78        if let Some(log_msg) = self.log_message {
79            match self.log_level {
80                LogLevel::Error => {
81                    tracing::error!(topic = "a2a_jsonrpc", "{}", log_msg);
82                },
83                LogLevel::Warn => {
84                    tracing::warn!(topic = "a2a_jsonrpc", "{}", log_msg);
85                },
86                LogLevel::Info => {
87                    tracing::info!(topic = "a2a_jsonrpc", "{}", log_msg);
88                },
89                LogLevel::Debug => {
90                    tracing::debug!(topic = "a2a_jsonrpc", "{}", log_msg);
91                },
92                LogLevel::Trace => {
93                    tracing::trace!(topic = "a2a_jsonrpc", "{}", log_msg);
94                },
95            }
96        }
97
98        let mut error = json!({
99            "code": self.code,
100            "message": self.message
101        });
102
103        if let Some(data) = self.data {
104            error["data"] = data;
105        }
106
107        json!({
108            "jsonrpc": "2.0",
109            "error": error,
110            "id": request_id
111        })
112    }
113
114    pub fn build_with_status(self, request_id: &NumberOrString) -> (StatusCode, Value) {
115        let status = match self.code {
116            -32600 => StatusCode::BAD_REQUEST,
117            -32601 => StatusCode::NOT_FOUND,
118            -32602 => StatusCode::BAD_REQUEST,
119            -32603 => StatusCode::INTERNAL_SERVER_ERROR,
120            -32700 => StatusCode::BAD_REQUEST,
121            _ => StatusCode::INTERNAL_SERVER_ERROR,
122        };
123
124        (status, self.build(request_id))
125    }
126
127    pub fn invalid_request() -> Self {
128        Self::new(-32600, "Invalid Request")
129    }
130
131    pub fn method_not_found() -> Self {
132        Self::new(-32601, "Method not found")
133    }
134
135    pub fn invalid_params() -> Self {
136        Self::new(-32602, "Invalid params")
137    }
138
139    pub fn internal_error() -> Self {
140        Self::new(-32603, "Internal error")
141    }
142
143    pub fn parse_error() -> Self {
144        Self::new(-32700, "Parse error")
145    }
146
147    pub fn unauthorized(reason: impl Into<String>) -> Self {
148        Self::new(-32600, "Unauthorized").with_data(json!({
149            "reason": reason.into()
150        }))
151    }
152
153    pub fn forbidden(reason: impl Into<String>) -> Self {
154        Self::new(-32600, "Forbidden").with_data(json!({
155            "reason": reason.into()
156        }))
157    }
158}
159
160pub fn unauthorized_response(
161    reason: impl Into<String>,
162    request_id: &NumberOrString,
163) -> (StatusCode, Value) {
164    let reason_str = reason.into();
165    (
166        StatusCode::UNAUTHORIZED,
167        JsonRpcErrorBuilder::unauthorized(&reason_str)
168            .log_warn(&reason_str)
169            .build(request_id),
170    )
171}
172
173pub fn forbidden_response(
174    reason: impl Into<String>,
175    request_id: &NumberOrString,
176) -> (StatusCode, Value) {
177    let reason_str = reason.into();
178    (
179        StatusCode::FORBIDDEN,
180        JsonRpcErrorBuilder::forbidden(&reason_str)
181            .log_warn(&reason_str)
182            .build(request_id),
183    )
184}