1use std::fmt;
13
14use a2a_protocol_types::error::{A2aError, ErrorCode};
15use a2a_protocol_types::task::TaskId;
16
17#[derive(Debug)]
23#[non_exhaustive]
24pub enum ServerError {
25 TaskNotFound(TaskId),
27 TaskNotCancelable(TaskId),
29 InvalidParams(String),
31 Serialization(serde_json::Error),
33 Http(hyper::Error),
35 HttpClient(String),
37 Transport(String),
39 PushNotSupported,
41 Internal(String),
43 MethodNotFound(String),
45 Protocol(A2aError),
47 PayloadTooLarge(String),
49 UnsupportedOperation(String),
52 InvalidStateTransition {
54 task_id: TaskId,
56 from: a2a_protocol_types::task::TaskState,
58 to: a2a_protocol_types::task::TaskState,
60 },
61 Overloaded(String),
65}
66
67impl fmt::Display for ServerError {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::TaskNotFound(id) => write!(f, "task not found: {id}"),
71 Self::TaskNotCancelable(id) => write!(f, "task not cancelable: {id}"),
72 Self::InvalidParams(msg) => write!(f, "invalid params: {msg}"),
73 Self::Serialization(e) => write!(f, "serialization error: {e}"),
74 Self::Http(e) => write!(f, "HTTP error: {e}"),
75 Self::HttpClient(msg) => write!(f, "HTTP client error: {msg}"),
76 Self::Transport(msg) => write!(f, "transport error: {msg}"),
77 Self::PushNotSupported => f.write_str("push notifications not supported"),
78 Self::UnsupportedOperation(msg) => write!(f, "unsupported operation: {msg}"),
79 Self::Internal(msg) => write!(f, "internal error: {msg}"),
80 Self::MethodNotFound(m) => write!(f, "method not found: {m}"),
81 Self::Protocol(e) => write!(f, "protocol error: {e}"),
82 Self::PayloadTooLarge(msg) => write!(f, "payload too large: {msg}"),
83 Self::InvalidStateTransition { task_id, from, to } => {
84 write!(
85 f,
86 "invalid state transition for task {task_id}: {from} → {to}"
87 )
88 }
89 Self::Overloaded(msg) => write!(f, "server overloaded: {msg}"),
90 }
91 }
92}
93
94impl std::error::Error for ServerError {
95 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
96 match self {
97 Self::Serialization(e) => Some(e),
98 Self::Http(e) => Some(e),
99 Self::Protocol(e) => Some(e),
100 _ => None,
101 }
102 }
103}
104
105impl ServerError {
106 #[must_use]
115 pub const fn metric_label(&self) -> &'static str {
116 match self {
117 Self::TaskNotFound(_) => "task_not_found",
118 Self::TaskNotCancelable(_) => "task_not_cancelable",
119 Self::InvalidParams(_) => "invalid_params",
120 Self::Serialization(_) => "serialization",
121 Self::Http(_) => "http",
122 Self::HttpClient(_) => "http_client",
123 Self::Transport(_) => "transport",
124 Self::PushNotSupported => "push_not_supported",
125 Self::Internal(_) => "internal",
126 Self::MethodNotFound(_) => "method_not_found",
127 Self::Protocol(_) => "protocol",
128 Self::PayloadTooLarge(_) => "payload_too_large",
129 Self::UnsupportedOperation(_) => "unsupported_operation",
130 Self::InvalidStateTransition { .. } => "invalid_state_transition",
131 Self::Overloaded(_) => "overloaded",
132 }
133 }
134
135 #[must_use]
150 pub fn to_a2a_error(&self) -> A2aError {
151 match self {
152 Self::TaskNotFound(id) => A2aError::task_not_found(id),
153 Self::TaskNotCancelable(id) => A2aError::task_not_cancelable(id),
154 Self::InvalidParams(msg) => A2aError::invalid_params(msg.clone()),
155 Self::Serialization(e) => A2aError::parse_error(e.to_string()),
156 Self::MethodNotFound(m) => {
157 A2aError::new(ErrorCode::MethodNotFound, format!("Method not found: {m}"))
158 }
159 Self::PushNotSupported => A2aError::new(
160 ErrorCode::PushNotificationNotSupported,
161 "Push notifications not supported",
162 ),
163 Self::UnsupportedOperation(msg) => {
164 A2aError::new(ErrorCode::UnsupportedOperation, msg.clone())
165 }
166 Self::Protocol(e) => e.clone(),
167 Self::Http(e) => A2aError::internal(e.to_string()),
168 Self::HttpClient(msg) | Self::Transport(msg) | Self::Internal(msg) => {
169 A2aError::internal(msg.clone())
170 }
171 Self::PayloadTooLarge(msg) => A2aError::new(ErrorCode::InvalidRequest, msg.clone()),
172 Self::InvalidStateTransition { task_id, from, to } => A2aError::invalid_params(
173 format!("invalid state transition for task {task_id}: {from} → {to}"),
174 ),
175 Self::Overloaded(msg) => A2aError::internal(msg.clone()),
180 }
181 }
182}
183
184impl From<A2aError> for ServerError {
187 fn from(e: A2aError) -> Self {
188 Self::Protocol(e)
189 }
190}
191
192impl From<serde_json::Error> for ServerError {
193 fn from(e: serde_json::Error) -> Self {
194 Self::Serialization(e)
195 }
196}
197
198impl From<hyper::Error> for ServerError {
199 fn from(e: hyper::Error) -> Self {
200 Self::Http(e)
201 }
202}
203
204pub type ServerResult<T> = Result<T, ServerError>;
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use std::error::Error;
213
214 #[test]
215 fn source_serialization_returns_some() {
216 let err = ServerError::Serialization(serde_json::from_str::<String>("x").unwrap_err());
217 assert!(err.source().is_some());
218 }
219
220 #[test]
221 fn source_protocol_returns_some() {
222 let err = ServerError::Protocol(A2aError::task_not_found("t"));
223 assert!(err.source().is_some());
224 }
225
226 #[tokio::test]
227 async fn source_http_returns_some() {
228 use tokio::io::AsyncWriteExt;
230 let (mut client, server) = tokio::io::duplex(256);
231 let client_task = tokio::spawn(async move {
233 client.write_all(b"NOT VALID HTTP\r\n\r\n").await.unwrap();
234 client.shutdown().await.unwrap();
235 });
236 let hyper_err = hyper::server::conn::http1::Builder::new()
237 .serve_connection(
238 hyper_util::rt::TokioIo::new(server),
239 hyper::service::service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
240 Ok::<_, hyper::Error>(hyper::Response::new(http_body_util::Full::new(
241 hyper::body::Bytes::new(),
242 )))
243 }),
244 )
245 .await
246 .unwrap_err();
247 client_task.await.unwrap();
248 let err = ServerError::Http(hyper_err);
249 assert!(err.source().is_some());
250 }
251
252 #[test]
253 fn source_transport_returns_none() {
254 let err = ServerError::Transport("test".into());
255 assert!(err.source().is_none());
256 }
257
258 #[test]
259 fn source_task_not_found_returns_none() {
260 let err = ServerError::TaskNotFound("t".into());
261 assert!(err.source().is_none());
262 }
263
264 #[test]
265 fn source_internal_returns_none() {
266 let err = ServerError::Internal("oops".into());
267 assert!(err.source().is_none());
268 }
269
270 #[test]
273 fn display_all_variants() {
274 assert!(ServerError::TaskNotFound("t1".into())
275 .to_string()
276 .contains("t1"));
277 assert!(ServerError::TaskNotCancelable("t2".into())
278 .to_string()
279 .contains("t2"));
280 assert!(ServerError::InvalidParams("bad".into())
281 .to_string()
282 .contains("bad"));
283 assert!(ServerError::HttpClient("conn".into())
284 .to_string()
285 .contains("conn"));
286 assert!(ServerError::Transport("tcp".into())
287 .to_string()
288 .contains("tcp"));
289 assert_eq!(
290 ServerError::PushNotSupported.to_string(),
291 "push notifications not supported"
292 );
293 assert!(ServerError::UnsupportedOperation("cannot do this".into())
294 .to_string()
295 .contains("cannot do this"));
296 assert!(ServerError::Internal("oops".into())
297 .to_string()
298 .contains("oops"));
299 assert!(ServerError::MethodNotFound("foo/bar".into())
300 .to_string()
301 .contains("foo/bar"));
302 assert!(ServerError::Protocol(A2aError::task_not_found("t"))
303 .to_string()
304 .contains("protocol error"));
305 assert!(ServerError::PayloadTooLarge("too big".into())
306 .to_string()
307 .contains("too big"));
308 let ist = ServerError::InvalidStateTransition {
309 task_id: "t3".into(),
310 from: a2a_protocol_types::task::TaskState::Working,
311 to: a2a_protocol_types::task::TaskState::Submitted,
312 };
313 let s = ist.to_string();
314 assert!(s.contains("t3"), "missing task_id: {s}");
315 assert!(
316 s.contains("working") || s.contains("WORKING") || s.contains("Working"),
317 "missing from state: {s}"
318 );
319 }
320
321 #[test]
324 #[allow(clippy::too_many_lines)]
325 fn to_a2a_error_all_variants() {
326 assert_eq!(
327 ServerError::TaskNotFound("t".into()).to_a2a_error().code,
328 ErrorCode::TaskNotFound
329 );
330 assert_eq!(
331 ServerError::TaskNotCancelable("t".into())
332 .to_a2a_error()
333 .code,
334 ErrorCode::TaskNotCancelable
335 );
336 assert_eq!(
337 ServerError::InvalidParams("x".into()).to_a2a_error().code,
338 ErrorCode::InvalidParams
339 );
340 assert_eq!(
341 ServerError::Serialization(serde_json::from_str::<String>("x").unwrap_err())
342 .to_a2a_error()
343 .code,
344 ErrorCode::ParseError
345 );
346 assert_eq!(
347 ServerError::MethodNotFound("m".into()).to_a2a_error().code,
348 ErrorCode::MethodNotFound
349 );
350 assert_eq!(
351 ServerError::PushNotSupported.to_a2a_error().code,
352 ErrorCode::PushNotificationNotSupported
353 );
354 assert_eq!(
355 ServerError::UnsupportedOperation("test".into())
356 .to_a2a_error()
357 .code,
358 ErrorCode::UnsupportedOperation
359 );
360 assert_eq!(
361 ServerError::Protocol(A2aError::task_not_found("t"))
362 .to_a2a_error()
363 .code,
364 ErrorCode::TaskNotFound
365 );
366 assert_eq!(
367 ServerError::HttpClient("x".into()).to_a2a_error().code,
368 ErrorCode::InternalError
369 );
370 assert_eq!(
371 ServerError::Transport("x".into()).to_a2a_error().code,
372 ErrorCode::InternalError
373 );
374 assert_eq!(
375 ServerError::Internal("x".into()).to_a2a_error().code,
376 ErrorCode::InternalError
377 );
378 assert_eq!(
379 ServerError::PayloadTooLarge("x".into()).to_a2a_error().code,
380 ErrorCode::InvalidRequest
381 );
382 let ist = ServerError::InvalidStateTransition {
383 task_id: "t".into(),
384 from: a2a_protocol_types::task::TaskState::Working,
385 to: a2a_protocol_types::task::TaskState::Submitted,
386 };
387 assert_eq!(ist.to_a2a_error().code, ErrorCode::InvalidParams);
388 }
389
390 #[test]
393 fn from_a2a_error() {
394 let e: ServerError = A2aError::internal("test").into();
395 assert!(matches!(e, ServerError::Protocol(_)));
396 }
397
398 #[test]
399 fn from_serde_error() {
400 let e: ServerError = serde_json::from_str::<String>("bad").unwrap_err().into();
401 assert!(matches!(e, ServerError::Serialization(_)));
402 }
403
404 #[tokio::test]
406 async fn display_http_variant() {
407 use tokio::io::AsyncWriteExt;
408 let (mut client, server) = tokio::io::duplex(256);
409 let client_task = tokio::spawn(async move {
410 client.write_all(b"NOT VALID HTTP\r\n\r\n").await.unwrap();
411 client.shutdown().await.unwrap();
412 });
413 let hyper_err = hyper::server::conn::http1::Builder::new()
414 .serve_connection(
415 hyper_util::rt::TokioIo::new(server),
416 hyper::service::service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
417 Ok::<_, hyper::Error>(hyper::Response::new(http_body_util::Full::new(
418 hyper::body::Bytes::new(),
419 )))
420 }),
421 )
422 .await
423 .unwrap_err();
424 client_task.await.unwrap();
425 let err = ServerError::Http(hyper_err);
426 let display = err.to_string();
427 assert!(
428 display.contains("HTTP error"),
429 "Display for Http variant should contain 'HTTP error', got: {display}"
430 );
431 }
432
433 #[tokio::test]
435 async fn from_hyper_error() {
436 use tokio::io::AsyncWriteExt;
437 let (mut client, server) = tokio::io::duplex(256);
438 let client_task = tokio::spawn(async move {
439 client.write_all(b"NOT VALID HTTP\r\n\r\n").await.unwrap();
440 client.shutdown().await.unwrap();
441 });
442 let hyper_err = hyper::server::conn::http1::Builder::new()
443 .serve_connection(
444 hyper_util::rt::TokioIo::new(server),
445 hyper::service::service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
446 Ok::<_, hyper::Error>(hyper::Response::new(http_body_util::Full::new(
447 hyper::body::Bytes::new(),
448 )))
449 }),
450 )
451 .await
452 .unwrap_err();
453 client_task.await.unwrap();
454 let e: ServerError = hyper_err.into();
455 assert!(matches!(e, ServerError::Http(_)));
456 }
457
458 #[test]
460 fn display_serialization_variant() {
461 let err = ServerError::Serialization(serde_json::from_str::<String>("x").unwrap_err());
462 let display = err.to_string();
463 assert!(
464 display.contains("serialization error"),
465 "Display for Serialization should contain 'serialization error', got: {display}"
466 );
467 }
468
469 #[tokio::test]
471 async fn to_a2a_error_http_variant() {
472 use tokio::io::AsyncWriteExt;
473 let (mut client, server) = tokio::io::duplex(256);
474 let client_task = tokio::spawn(async move {
475 client.write_all(b"NOT VALID HTTP\r\n\r\n").await.unwrap();
476 client.shutdown().await.unwrap();
477 });
478 let hyper_err = hyper::server::conn::http1::Builder::new()
479 .serve_connection(
480 hyper_util::rt::TokioIo::new(server),
481 hyper::service::service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
482 Ok::<_, hyper::Error>(hyper::Response::new(http_body_util::Full::new(
483 hyper::body::Bytes::new(),
484 )))
485 }),
486 )
487 .await
488 .unwrap_err();
489 client_task.await.unwrap();
490 let err = ServerError::Http(hyper_err);
491 let a2a_err = err.to_a2a_error();
492 assert_eq!(a2a_err.code, ErrorCode::InternalError);
493 }
494}