Skip to main content

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}