1use std::collections::BTreeMap;
2
3use http::StatusCode;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7
8use crate::jsonrpc;
9use crate::jsonrpc::JsonRpcError;
10
11pub const ERROR_INFO_TYPE_URL: &str = "type.googleapis.com/google.rpc.ErrorInfo";
13pub const ERROR_INFO_DOMAIN: &str = "a2a-protocol.org";
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct ErrorInfo {
19 #[serde(rename = "@type", default = "error_info_type_url")]
21 pub type_url: String,
22 pub reason: String,
24 pub domain: String,
26 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
28 pub metadata: BTreeMap<String, String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ProblemDetails {
34 #[serde(rename = "type")]
36 pub type_url: String,
37 pub title: String,
39 pub status: u16,
41 pub detail: String,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub reason: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub domain: Option<String>,
49 #[serde(default, flatten, skip_serializing_if = "BTreeMap::is_empty")]
51 pub extensions: BTreeMap<String, Value>,
52}
53
54#[derive(Debug, Error)]
56pub enum A2AError {
57 #[error("task not found: {0}")]
59 TaskNotFound(String),
60 #[error("task not cancelable: {0}")]
62 TaskNotCancelable(String),
63 #[error("push notification not supported: {0}")]
65 PushNotificationNotSupported(String),
66 #[error("unsupported operation: {0}")]
68 UnsupportedOperation(String),
69 #[error("content type not supported: {0}")]
71 ContentTypeNotSupported(String),
72 #[error("invalid agent response: {0}")]
74 InvalidAgentResponse(String),
75 #[error("extended agent card not configured: {0}")]
77 ExtendedAgentCardNotConfigured(String),
78 #[error("extension support required: {0}")]
80 ExtensionSupportRequired(String),
81 #[error("version not supported: {0}")]
83 VersionNotSupported(String),
84 #[error("parse error: {0}")]
86 ParseError(String),
87 #[error("invalid request: {0}")]
89 InvalidRequest(String),
90 #[error("method not found: {0}")]
92 MethodNotFound(String),
93 #[error("invalid params: {0}")]
95 InvalidParams(String),
96 #[error("internal error: {0}")]
98 Internal(String),
99 #[error("serialization error: {0}")]
101 Serialization(#[from] serde_json::Error),
102 #[cfg(feature = "client")]
103 #[error("http error: {0}")]
105 Http(#[from] reqwest::Error),
106}
107
108impl A2AError {
109 pub fn reason(&self) -> &'static str {
111 match self {
112 Self::TaskNotFound(_) => "TASK_NOT_FOUND",
113 Self::TaskNotCancelable(_) => "TASK_NOT_CANCELABLE",
114 Self::PushNotificationNotSupported(_) => "PUSH_NOTIFICATION_NOT_SUPPORTED",
115 Self::UnsupportedOperation(_) => "UNSUPPORTED_OPERATION",
116 Self::ContentTypeNotSupported(_) => "CONTENT_TYPE_NOT_SUPPORTED",
117 Self::InvalidAgentResponse(_) => "INVALID_AGENT_RESPONSE",
118 Self::ExtendedAgentCardNotConfigured(_) => "EXTENDED_AGENT_CARD_NOT_CONFIGURED",
119 Self::ExtensionSupportRequired(_) => "EXTENSION_SUPPORT_REQUIRED",
120 Self::VersionNotSupported(_) => "VERSION_NOT_SUPPORTED",
121 Self::ParseError(_) => "PARSE_ERROR",
122 Self::InvalidRequest(_) => "INVALID_REQUEST",
123 Self::MethodNotFound(_) => "METHOD_NOT_FOUND",
124 Self::InvalidParams(_) => "INVALID_PARAMS",
125 Self::Internal(_) | Self::Serialization(_) => "INTERNAL",
126 #[cfg(feature = "client")]
127 Self::Http(_) => "HTTP",
128 }
129 }
130
131 pub fn code(&self) -> i32 {
133 match self {
134 Self::TaskNotFound(_) => jsonrpc::TASK_NOT_FOUND,
135 Self::TaskNotCancelable(_) => jsonrpc::TASK_NOT_CANCELABLE,
136 Self::PushNotificationNotSupported(_) => jsonrpc::PUSH_NOTIFICATION_NOT_SUPPORTED,
137 Self::UnsupportedOperation(_) => jsonrpc::UNSUPPORTED_OPERATION,
138 Self::ContentTypeNotSupported(_) => jsonrpc::CONTENT_TYPE_NOT_SUPPORTED,
139 Self::InvalidAgentResponse(_) => jsonrpc::INVALID_AGENT_RESPONSE,
140 Self::ExtendedAgentCardNotConfigured(_) => jsonrpc::EXTENDED_AGENT_CARD_NOT_CONFIGURED,
141 Self::ExtensionSupportRequired(_) => jsonrpc::EXTENSION_SUPPORT_REQUIRED,
142 Self::VersionNotSupported(_) => jsonrpc::VERSION_NOT_SUPPORTED,
143 Self::ParseError(_) => jsonrpc::PARSE_ERROR,
144 Self::InvalidRequest(_) => jsonrpc::INVALID_REQUEST,
145 Self::MethodNotFound(_) => jsonrpc::METHOD_NOT_FOUND,
146 Self::InvalidParams(_) => jsonrpc::INVALID_PARAMS,
147 Self::Internal(_) => jsonrpc::INTERNAL_ERROR,
148 Self::Serialization(_) => jsonrpc::INTERNAL_ERROR,
149 #[cfg(feature = "client")]
150 Self::Http(_) => jsonrpc::INTERNAL_ERROR,
151 }
152 }
153
154 pub fn to_jsonrpc_error(&self) -> JsonRpcError {
156 JsonRpcError {
157 code: self.code(),
158 message: self.to_string(),
159 data: Some(
160 serde_json::to_value(self.to_error_info()).expect("error details should serialize"),
161 ),
162 }
163 }
164
165 pub fn to_problem_details(&self) -> ProblemDetails {
167 let status_code = self.status_code();
168 ProblemDetails {
169 type_url: self.problem_type_url().to_owned(),
170 title: self.problem_title().to_owned(),
171 status: status_code.as_u16(),
172 detail: self.to_string(),
173 reason: Some(self.reason().to_owned()),
174 domain: Some(ERROR_INFO_DOMAIN.to_owned()),
175 extensions: self
176 .metadata()
177 .into_iter()
178 .map(|(key, value)| (key, Value::String(value)))
179 .collect(),
180 }
181 }
182
183 pub fn status_code(&self) -> StatusCode {
185 match self {
186 Self::TaskNotFound(_) => StatusCode::NOT_FOUND,
187 Self::TaskNotCancelable(_) => StatusCode::CONFLICT,
188 Self::PushNotificationNotSupported(_) => StatusCode::BAD_REQUEST,
189 Self::UnsupportedOperation(_) => StatusCode::BAD_REQUEST,
190 Self::ContentTypeNotSupported(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
191 Self::InvalidAgentResponse(_) => StatusCode::BAD_GATEWAY,
192 Self::ExtendedAgentCardNotConfigured(_) => StatusCode::BAD_REQUEST,
193 Self::ExtensionSupportRequired(_) => StatusCode::BAD_REQUEST,
194 Self::VersionNotSupported(_) => StatusCode::BAD_REQUEST,
195 Self::ParseError(_) => StatusCode::BAD_REQUEST,
196 Self::InvalidRequest(_) => StatusCode::BAD_REQUEST,
197 Self::MethodNotFound(_) => StatusCode::NOT_FOUND,
198 Self::InvalidParams(_) => StatusCode::BAD_REQUEST,
199 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
200 Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
201 #[cfg(feature = "client")]
202 Self::Http(_) => StatusCode::BAD_GATEWAY,
203 }
204 }
205
206 pub fn to_error_info(&self) -> ErrorInfo {
208 ErrorInfo {
209 type_url: error_info_type_url(),
210 reason: self.reason().to_owned(),
211 domain: ERROR_INFO_DOMAIN.to_owned(),
212 metadata: self.metadata(),
213 }
214 }
215
216 pub fn from_problem_details(problem: &ProblemDetails) -> Self {
218 let reason = problem
219 .reason
220 .clone()
221 .unwrap_or_else(|| problem_reason(problem.type_url.as_str()).to_owned());
222 let info = ErrorInfo {
223 type_url: error_info_type_url(),
224 reason: reason.clone(),
225 domain: problem
226 .domain
227 .clone()
228 .unwrap_or_else(|| ERROR_INFO_DOMAIN.to_owned()),
229 metadata: problem
230 .extensions
231 .iter()
232 .filter_map(|(key, value)| match value {
233 Value::String(value) => Some((key.clone(), value.clone())),
234 Value::Number(value) => Some((key.clone(), value.to_string())),
235 Value::Bool(value) => Some((key.clone(), value.to_string())),
236 _ => None,
237 })
238 .collect(),
239 };
240
241 Self::from_error_info(reason_code(reason.as_str()), &problem.detail, Some(&info))
242 }
243
244 pub fn from_error_info(error_code: i32, message: &str, info: Option<&ErrorInfo>) -> Self {
246 let fallback_detail = info
247 .and_then(|info| info.metadata.get("detail").cloned())
248 .unwrap_or_else(|| message.to_owned());
249
250 let reason = info.map(|info| info.reason.as_str()).unwrap_or("");
251 let metadata = info.map(|info| &info.metadata);
252
253 match (error_code, reason) {
254 (jsonrpc::TASK_NOT_FOUND, "TASK_NOT_FOUND") => Self::TaskNotFound(
255 metadata
256 .and_then(|metadata| metadata.get("taskId").cloned())
257 .unwrap_or(fallback_detail),
258 ),
259 (jsonrpc::TASK_NOT_CANCELABLE, "TASK_NOT_CANCELABLE") => Self::TaskNotCancelable(
260 metadata
261 .and_then(|metadata| metadata.get("taskId").cloned())
262 .unwrap_or(fallback_detail),
263 ),
264 (jsonrpc::PUSH_NOTIFICATION_NOT_SUPPORTED, _) => {
265 Self::PushNotificationNotSupported(fallback_detail)
266 }
267 (jsonrpc::UNSUPPORTED_OPERATION, _) => Self::UnsupportedOperation(fallback_detail),
268 (jsonrpc::CONTENT_TYPE_NOT_SUPPORTED, _) => {
269 Self::ContentTypeNotSupported(fallback_detail)
270 }
271 (jsonrpc::INVALID_AGENT_RESPONSE, _) => Self::InvalidAgentResponse(fallback_detail),
272 (jsonrpc::EXTENDED_AGENT_CARD_NOT_CONFIGURED, _) => {
273 Self::ExtendedAgentCardNotConfigured(fallback_detail)
274 }
275 (jsonrpc::EXTENSION_SUPPORT_REQUIRED, _) => {
276 Self::ExtensionSupportRequired(fallback_detail)
277 }
278 (jsonrpc::VERSION_NOT_SUPPORTED, _) => Self::VersionNotSupported(fallback_detail),
279 (jsonrpc::PARSE_ERROR, _) => Self::ParseError(fallback_detail),
280 (jsonrpc::INVALID_REQUEST, _) => Self::InvalidRequest(fallback_detail),
281 (jsonrpc::METHOD_NOT_FOUND, _) => Self::MethodNotFound(fallback_detail),
282 (jsonrpc::INVALID_PARAMS, _) => Self::InvalidParams(fallback_detail),
283 (jsonrpc::INTERNAL_ERROR, _) => Self::Internal(fallback_detail),
284 _ => Self::Internal(fallback_detail),
285 }
286 }
287
288 fn problem_type_url(&self) -> &'static str {
289 match self {
290 Self::TaskNotFound(_) => "https://a2a-protocol.org/errors/task-not-found",
291 Self::TaskNotCancelable(_) => "https://a2a-protocol.org/errors/task-not-cancelable",
292 Self::PushNotificationNotSupported(_) => {
293 "https://a2a-protocol.org/errors/push-notification-not-supported"
294 }
295 Self::UnsupportedOperation(_) => {
296 "https://a2a-protocol.org/errors/unsupported-operation"
297 }
298 Self::ContentTypeNotSupported(_) => {
299 "https://a2a-protocol.org/errors/content-type-not-supported"
300 }
301 Self::InvalidAgentResponse(_) => {
302 "https://a2a-protocol.org/errors/invalid-agent-response"
303 }
304 Self::ExtendedAgentCardNotConfigured(_) => {
305 "https://a2a-protocol.org/errors/extended-agent-card-not-configured"
306 }
307 Self::ExtensionSupportRequired(_) => {
308 "https://a2a-protocol.org/errors/extension-support-required"
309 }
310 Self::VersionNotSupported(_) => "https://a2a-protocol.org/errors/version-not-supported",
311 Self::ParseError(_) => "about:blank",
312 Self::InvalidRequest(_) => "about:blank",
313 Self::MethodNotFound(_) => "about:blank",
314 Self::InvalidParams(_) => "about:blank",
315 Self::Internal(_) | Self::Serialization(_) => "about:blank",
316 #[cfg(feature = "client")]
317 Self::Http(_) => "about:blank",
318 }
319 }
320
321 fn problem_title(&self) -> &'static str {
322 match self {
323 Self::TaskNotFound(_) => "Task not found",
324 Self::TaskNotCancelable(_) => "Task not cancelable",
325 Self::PushNotificationNotSupported(_) => "Push notifications not supported",
326 Self::UnsupportedOperation(_) => "Unsupported operation",
327 Self::ContentTypeNotSupported(_) => "Content type not supported",
328 Self::InvalidAgentResponse(_) => "Invalid agent response",
329 Self::ExtendedAgentCardNotConfigured(_) => "Extended agent card not configured",
330 Self::ExtensionSupportRequired(_) => "Extension support required",
331 Self::VersionNotSupported(_) => "Version not supported",
332 Self::ParseError(_) => "Bad Request",
333 Self::InvalidRequest(_) => "Bad Request",
334 Self::MethodNotFound(_) => "Not Found",
335 Self::InvalidParams(_) => "Bad Request",
336 Self::Internal(_) | Self::Serialization(_) => "Internal Server Error",
337 #[cfg(feature = "client")]
338 Self::Http(_) => "Bad Gateway",
339 }
340 }
341
342 fn metadata(&self) -> BTreeMap<String, String> {
343 let mut metadata = BTreeMap::new();
344
345 match self {
346 Self::TaskNotFound(task_id) | Self::TaskNotCancelable(task_id) => {
347 metadata.insert("taskId".to_owned(), task_id.clone());
348 }
349 Self::PushNotificationNotSupported(detail)
350 | Self::UnsupportedOperation(detail)
351 | Self::ContentTypeNotSupported(detail)
352 | Self::InvalidAgentResponse(detail)
353 | Self::ExtendedAgentCardNotConfigured(detail)
354 | Self::ExtensionSupportRequired(detail)
355 | Self::VersionNotSupported(detail)
356 | Self::ParseError(detail)
357 | Self::InvalidRequest(detail)
358 | Self::MethodNotFound(detail)
359 | Self::InvalidParams(detail)
360 | Self::Internal(detail) => {
361 metadata.insert("detail".to_owned(), detail.clone());
362 }
363 Self::Serialization(error) => {
364 metadata.insert("detail".to_owned(), error.to_string());
365 }
366 #[cfg(feature = "client")]
367 Self::Http(error) => {
368 metadata.insert("detail".to_owned(), error.to_string());
369 }
370 }
371
372 metadata
373 }
374}
375
376impl ProblemDetails {
377 pub fn to_a2a_error(&self) -> A2AError {
379 A2AError::from_problem_details(self)
380 }
381}
382
383impl JsonRpcError {
384 pub fn first_error_info(&self) -> Option<ErrorInfo> {
386 match self.data.as_ref()? {
387 Value::Array(details) => details
388 .iter()
389 .find_map(|detail| serde_json::from_value::<ErrorInfo>(detail.clone()).ok()),
390 Value::Object(_) => serde_json::from_value::<ErrorInfo>(self.data.clone()?).ok(),
391 _ => None,
392 }
393 }
394}
395
396fn error_info_type_url() -> String {
397 ERROR_INFO_TYPE_URL.to_owned()
398}
399
400fn problem_code(type_url: &str) -> i32 {
401 match type_url {
402 "https://a2a-protocol.org/errors/task-not-found" => jsonrpc::TASK_NOT_FOUND,
403 "https://a2a-protocol.org/errors/task-not-cancelable" => jsonrpc::TASK_NOT_CANCELABLE,
404 "https://a2a-protocol.org/errors/push-notification-not-supported" => {
405 jsonrpc::PUSH_NOTIFICATION_NOT_SUPPORTED
406 }
407 "https://a2a-protocol.org/errors/unsupported-operation" => jsonrpc::UNSUPPORTED_OPERATION,
408 "https://a2a-protocol.org/errors/content-type-not-supported" => {
409 jsonrpc::CONTENT_TYPE_NOT_SUPPORTED
410 }
411 "https://a2a-protocol.org/errors/invalid-agent-response" => jsonrpc::INVALID_AGENT_RESPONSE,
412 "https://a2a-protocol.org/errors/extended-agent-card-not-configured" => {
413 jsonrpc::EXTENDED_AGENT_CARD_NOT_CONFIGURED
414 }
415 "https://a2a-protocol.org/errors/extension-support-required" => {
416 jsonrpc::EXTENSION_SUPPORT_REQUIRED
417 }
418 "https://a2a-protocol.org/errors/version-not-supported" => jsonrpc::VERSION_NOT_SUPPORTED,
419 _ => jsonrpc::INTERNAL_ERROR,
420 }
421}
422
423fn problem_reason(type_url: &str) -> &'static str {
424 match problem_code(type_url) {
425 jsonrpc::TASK_NOT_FOUND => "TASK_NOT_FOUND",
426 jsonrpc::TASK_NOT_CANCELABLE => "TASK_NOT_CANCELABLE",
427 jsonrpc::PUSH_NOTIFICATION_NOT_SUPPORTED => "PUSH_NOTIFICATION_NOT_SUPPORTED",
428 jsonrpc::UNSUPPORTED_OPERATION => "UNSUPPORTED_OPERATION",
429 jsonrpc::CONTENT_TYPE_NOT_SUPPORTED => "CONTENT_TYPE_NOT_SUPPORTED",
430 jsonrpc::INVALID_AGENT_RESPONSE => "INVALID_AGENT_RESPONSE",
431 jsonrpc::EXTENDED_AGENT_CARD_NOT_CONFIGURED => "EXTENDED_AGENT_CARD_NOT_CONFIGURED",
432 jsonrpc::EXTENSION_SUPPORT_REQUIRED => "EXTENSION_SUPPORT_REQUIRED",
433 jsonrpc::VERSION_NOT_SUPPORTED => "VERSION_NOT_SUPPORTED",
434 jsonrpc::PARSE_ERROR => "PARSE_ERROR",
435 jsonrpc::INVALID_REQUEST => "INVALID_REQUEST",
436 jsonrpc::METHOD_NOT_FOUND => "METHOD_NOT_FOUND",
437 jsonrpc::INVALID_PARAMS => "INVALID_PARAMS",
438 _ => "INTERNAL",
439 }
440}
441
442fn reason_code(reason: &str) -> i32 {
443 match reason {
444 "TASK_NOT_FOUND" => jsonrpc::TASK_NOT_FOUND,
445 "TASK_NOT_CANCELABLE" => jsonrpc::TASK_NOT_CANCELABLE,
446 "PUSH_NOTIFICATION_NOT_SUPPORTED" => jsonrpc::PUSH_NOTIFICATION_NOT_SUPPORTED,
447 "UNSUPPORTED_OPERATION" => jsonrpc::UNSUPPORTED_OPERATION,
448 "CONTENT_TYPE_NOT_SUPPORTED" => jsonrpc::CONTENT_TYPE_NOT_SUPPORTED,
449 "INVALID_AGENT_RESPONSE" => jsonrpc::INVALID_AGENT_RESPONSE,
450 "EXTENDED_AGENT_CARD_NOT_CONFIGURED" => jsonrpc::EXTENDED_AGENT_CARD_NOT_CONFIGURED,
451 "EXTENSION_SUPPORT_REQUIRED" => jsonrpc::EXTENSION_SUPPORT_REQUIRED,
452 "VERSION_NOT_SUPPORTED" => jsonrpc::VERSION_NOT_SUPPORTED,
453 "PARSE_ERROR" => jsonrpc::PARSE_ERROR,
454 "INVALID_REQUEST" => jsonrpc::INVALID_REQUEST,
455 "METHOD_NOT_FOUND" => jsonrpc::METHOD_NOT_FOUND,
456 "INVALID_PARAMS" => jsonrpc::INVALID_PARAMS,
457 _ => jsonrpc::INTERNAL_ERROR,
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::{A2AError, ERROR_INFO_DOMAIN, ERROR_INFO_TYPE_URL};
464
465 #[test]
466 fn jsonrpc_error_uses_structured_error_info_object() {
467 let error = A2AError::TaskNotFound("task-1".to_owned()).to_jsonrpc_error();
468
469 assert_eq!(error.code, crate::jsonrpc::TASK_NOT_FOUND);
470 assert_eq!(
471 error.data,
472 Some(serde_json::json!({
473 "@type": ERROR_INFO_TYPE_URL,
474 "reason": "TASK_NOT_FOUND",
475 "domain": ERROR_INFO_DOMAIN,
476 "metadata": {
477 "taskId": "task-1",
478 }
479 }))
480 );
481 }
482
483 #[test]
484 fn problem_details_round_trip_to_a2a_error() {
485 let error = A2AError::ExtensionSupportRequired("missing extension".to_owned());
486 let problem = error.to_problem_details();
487
488 assert_eq!(
489 A2AError::from_problem_details(&problem).to_string(),
490 error.to_string()
491 );
492 }
493}