trql_client/error.rs
1//! The client's error taxonomy.
2//!
3//! Variants separate the three failure classes an operator must be able to
4//! tell apart: the transport failed (retry / check connectivity), the registry
5//! rejected the task (act on the machine-readable code), or one side broke the
6//! wire contract (a bug — never retried).
7
8use chrono::{DateTime, Utc};
9use trust_tasks_rs::TrustTaskCode;
10
11use crate::transport::TransportKind;
12
13/// Errors returned by [`crate::TrqlClient`] and [`crate::TrqlTransport`]
14/// implementations.
15#[derive(Debug, thiserror::Error)]
16pub enum TrqlError {
17 /// The client or transport was built with missing/invalid configuration.
18 #[error("configuration error: {0}")]
19 Config(String),
20
21 /// The transport could not complete the exchange (connect, send, or
22 /// receive failure). Retryable at the caller's discretion.
23 #[error("{kind} transport error: {detail}")]
24 Transport {
25 /// Which binding failed.
26 kind: TransportKind,
27 /// What actually happened, named per failure (never a generic hint).
28 detail: String,
29 },
30
31 /// No reply arrived within the transport's configured reply window.
32 #[error("{kind} reply timed out after {waited_secs}s")]
33 Timeout {
34 /// Which binding timed out.
35 kind: TransportKind,
36 /// How long the transport waited.
37 waited_secs: u64,
38 },
39
40 /// The registry answered with a `trust-task-error` document.
41 #[error("registry rejected the task ({code}): {}", message.as_deref().unwrap_or("no detail"))]
42 Rejected {
43 /// Machine-readable trust-task error code.
44 code: TrustTaskCode,
45 /// Whether the registry marked the failure retryable.
46 retryable: bool,
47 /// Earliest retry instant, when the registry supplied one.
48 retry_after: Option<DateTime<Utc>>,
49 /// Human-readable detail from the registry, if any.
50 message: Option<String>,
51 },
52
53 /// The peer's reply violated the Trust Task contract: it didn't parse,
54 /// wasn't correlated to the request, or carried an unexpected type. This
55 /// is a bug on one side of the wire, not a transient condition — it is
56 /// never worth retrying.
57 #[error("contract violation: {0}")]
58 Contract(String),
59
60 /// The registry advertises no transport this client also speaks.
61 ///
62 /// Carries both sides' sets so an operator can see what to enable rather
63 /// than guess. Never a silent downgrade to a transport the registry did
64 /// not advertise.
65 #[error(
66 "no shared transport with the registry (we speak [{}], it advertises [{}])",
67 format_kinds(ours),
68 format_kinds(theirs)
69 )]
70 NoMatchingTransport {
71 /// Transports this client can use (compiled in and offered).
72 ours: Vec<TransportKind>,
73 /// Transports the registry advertises in its DID document.
74 theirs: Vec<TransportKind>,
75 },
76}
77
78/// Render a transport list for an error message; `"none"` when empty, so a
79/// registry advertising nothing reads clearly rather than as `[]`.
80fn format_kinds(kinds: &[TransportKind]) -> String {
81 if kinds.is_empty() {
82 return "none".to_string();
83 }
84 kinds
85 .iter()
86 .map(ToString::to_string)
87 .collect::<Vec<_>>()
88 .join(", ")
89}
90
91impl TrqlError {
92 /// Whether retrying the same query could plausibly succeed.
93 ///
94 /// Contract violations and configuration errors are never retryable; a
95 /// registry rejection is retryable only when the registry said so.
96 pub fn is_retryable(&self) -> bool {
97 match self {
98 Self::Transport { .. } | Self::Timeout { .. } => true,
99 Self::Rejected { retryable, .. } => *retryable,
100 // A capability mismatch is a deployment fact, not a transient
101 // condition: retrying the same pair of DID documents cannot help.
102 Self::Config(_) | Self::Contract(_) | Self::NoMatchingTransport { .. } => false,
103 }
104 }
105}