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