157 lines
5.1 KiB
Markdown
157 lines
5.1 KiB
Markdown
|
|
# 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)
|