Vapora/docs/adrs/0032-a2a-error-handling-json-rpc.md

157 lines
5.1 KiB
Markdown
Raw Normal View History

2026-02-17 13:18:12 +00:00
# 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": <domain-code>, "message": <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<T, A2aError>` 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)