Vapora/docs/adrs/0032-a2a-error-handling-json-rpc.md
Jesús Pérez 0b78d97fd7
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
Documentation Lint & Validation / Markdown Linting (push) Has been cancelled
Documentation Lint & Validation / Validate mdBook Configuration (push) Has been cancelled
Documentation Lint & Validation / Content & Structure Validation (push) Has been cancelled
Documentation Lint & Validation / Lint & Validation Summary (push) Has been cancelled
mdBook Build & Deploy / Build mdBook (push) Has been cancelled
mdBook Build & Deploy / Documentation Quality Check (push) Has been cancelled
mdBook Build & Deploy / Deploy to GitHub Pages (push) Has been cancelled
mdBook Build & Deploy / Notification (push) Has been cancelled
chore: update adrs
2026-02-17 13:18:12 +00:00

5.1 KiB

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):

// 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):

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.rsA2aError + to_json_rpc_error()
  • crates/vapora-a2a-client/src/error.rsA2aClientError
  • crates/vapora-a2a-client/src/retry.rs — Error classification for retry policy

Error flow:

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

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

Related ADRs:

  • ADR-0030 — A2A protocol (server that produces these errors)
  • ADR-0022 — General two-tier error handling pattern (this ADR specializes it for A2A/JSON-RPC)