# ADR-0032: A2A Error Handling and JSON-RPC 2.0 Compliance **Status**: Implemented **Date**: 2026-02-07 **Deciders**: VAPORA Team **Technical Story**: Consistent, specification-compliant error representation across the A2A client/server boundary --- ## Decision Two-layer error handling strategy for the A2A subsystem: **Layer 1 — Domain errors (Rust `thiserror`):** ```rust // vapora-a2a pub enum A2aError { TaskNotFound(String), InvalidStateTransition { current: String, target: String }, CoordinatorError(String), UnknownSkill(String), SerdeError, IoError, InternalError(String), } // vapora-a2a-client pub enum A2aClientError { HttpError, TaskNotFound(String), ServerError { code: i32, message: String }, ConnectionRefused(String), Timeout(String), InvalidResponse, InternalError(String), } ``` **Layer 2 — Protocol serialization (JSON-RPC 2.0):** ```rust impl A2aError { pub fn to_json_rpc_error(&self) -> serde_json::Value { json!({ "jsonrpc": "2.0", "error": { "code": , "message": } }) } } ``` **Error code mapping:** | Category | JSON-RPC Code | A2aError variants | |---|---|---| | Domain / server errors | -32000 | `TaskNotFound`, `UnknownSkill`, `InvalidStateTransition` | | Internal errors | -32603 | `SerdeError`, `IoError`, `InternalError` | | Parse errors | -32700 | Handled by JSON parser | | Invalid request | -32600 | Handled by Axum | --- ## Rationale **Why two layers?** Domain layer gives type-safe `Result` propagation throughout the crate. Protocol layer isolates JSON-RPC specifics to conversion methods — domain code has no protocol awareness. **Why JSON-RPC 2.0 standard codes?** Code ranges are defined by the specification and understood by compliant clients without custom documentation. Enables generic error handling on the client side. **Why `thiserror`?** Minimal boilerplate. Automatic `Display` derives. Composes cleanly with `?`. Validated pattern throughout the VAPORA codebase (ADR-0022). **Why one-way conversion (domain → protocol)?** Protocol details cannot bleed into domain code. Future protocol changes are contained to conversion methods. Each layer is independently testable. --- ## Alternatives Considered **Custom error codes** — rejected: non-standard, client libraries can't handle them generically, harder to debug. **Single error type** — rejected: collapses domain semantics into protocol representation, loses type safety, makes specific error handling impossible. **No protocol conversion (raw Rust errors as HTTP 500)** — rejected: violates JSON-RPC 2.0 compliance, breaks A2A client expectations, prevents interoperability. --- ## Trade-offs **Pros:** - Compile-time exhaustive error handling via `match` - Protocol compliance verified: clients receive spec-compliant `{"jsonrpc":"2.0","error":{...}}` - Error flow is auditable — each variant maps to exactly one JSON-RPC code - Contextual tracing: all errors logged with `task_id`, `operation`, error message - Client retry logic (`RetryPolicy`) classifies errors from JSON-RPC codes: 5xx retried, 4xx not retried **Cons:** - Some error context is intentionally lost in translation (internal detail not exposed to clients) - JSON-RPC code documentation must be kept in sync with new variants - Boundary conversions require explicit calls at each Axum handler --- ## Implementation **Key files:** - `crates/vapora-a2a/src/error.rs` — `A2aError` + `to_json_rpc_error()` - `crates/vapora-a2a-client/src/error.rs` — `A2aClientError` - `crates/vapora-a2a-client/src/retry.rs` — Error classification for retry policy **Error flow:** ```text HTTP request → Axum handler → TaskManager::get(id) → Err(A2aError::TaskNotFound) → to_json_rpc_error() → {"jsonrpc":"2.0","error":{"code":-32000,...}} → (StatusCode::NOT_FOUND, Json(error_body)) ← vapora-a2a-client parses → A2aClientError::TaskNotFound ← caller matches variant ``` --- ## Verification ```bash cargo test -p vapora-a2a # error conversion tests cargo test -p vapora-a2a-client # 5/5 pass (includes retry classification) cargo clippy -p vapora-a2a -- -D warnings cargo clippy -p vapora-a2a-client -- -D warnings ``` --- ## Consequences - All new A2A error variants must be added to both `A2aError` and the JSON-RPC code mapping table - `A2aClientError` must mirror any new server-side variants that clients need to handle specifically - Pattern is scoped to the A2A subsystem; general VAPORA error handling follows ADR-0022 --- ## References - `crates/vapora-a2a/src/error.rs` - `crates/vapora-a2a-client/src/error.rs` - [thiserror](https://docs.rs/thiserror/) - [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification) - [Axum error responses](https://docs.rs/axum/latest/axum/response/index.html) **Related ADRs:** - [ADR-0030](./0030-a2a-protocol-implementation.md) — A2A protocol (server that produces these errors) - [ADR-0022](./0022-error-handling.md) — General two-tier error handling pattern (this ADR specializes it for A2A/JSON-RPC)