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 reply is correlated to our request but answers a *different* TRQP
61    /// tuple than the one we asked about.
62    ///
63    /// Correlation proves the reply belongs to our exchange; it says nothing
64    /// about what the reply is an answer to. A registry that echoes a
65    /// different `entity_id`/`authority_id`/`action`/`resource` has answered
66    /// someone else's question, and treating it as ours silently substitutes
67    /// the subject of an authorization decision.
68    #[error("registry answered for a different {field}: asked `{asked}`, answered `{answered}`")]
69    AnswerMismatch {
70        /// Which tuple member disagreed.
71        field: &'static str,
72        /// The value we sent on the request.
73        asked: String,
74        /// The value the registry echoed back.
75        answered: String,
76    },
77
78    /// We followed a referral to this registry, and its answer does not claim
79    /// the authority that referred us.
80    ///
81    /// A `TrustRegistry` referral in a VTC's DID document is a
82    /// self-assertion — anyone may publish a document naming any registry.
83    /// Authority flows registry → subject, so a referral is only closed when
84    /// the registry answers *for* the DID we started from. Until then the
85    /// referral has established where to ask and nothing about the answer.
86    #[error(
87        "referral from `{origin}` not closed: registry answered for authority `{answered}`, \
88         which does not confirm the referral"
89    )]
90    ReferralNotClosed {
91        /// The DID whose document referred us here.
92        origin: String,
93        /// The authority the registry actually answered for.
94        answered: String,
95    },
96
97    /// The registry advertises no transport this client also speaks.
98    ///
99    /// Carries both sides' sets so an operator can see what to enable rather
100    /// than guess. Never a silent downgrade to a transport the registry did
101    /// not advertise.
102    #[error(
103        "no shared transport with the registry (we speak [{}], it advertises [{}])",
104        format_kinds(ours),
105        format_kinds(theirs)
106    )]
107    NoMatchingTransport {
108        /// Transports this client can use (compiled in and offered).
109        ours: Vec<TransportKind>,
110        /// Transports the registry advertises in its DID document.
111        theirs: Vec<TransportKind>,
112    },
113}
114
115/// Render a transport list for an error message; `"none"` when empty, so a
116/// registry advertising nothing reads clearly rather than as `[]`.
117fn format_kinds(kinds: &[TransportKind]) -> String {
118    if kinds.is_empty() {
119        return "none".to_string();
120    }
121    kinds
122        .iter()
123        .map(ToString::to_string)
124        .collect::<Vec<_>>()
125        .join(", ")
126}
127
128impl TrqlError {
129    /// Whether retrying the same query could plausibly succeed.
130    ///
131    /// Contract violations and configuration errors are never retryable; a
132    /// registry rejection is retryable only when the registry said so.
133    pub fn is_retryable(&self) -> bool {
134        match self {
135            Self::Transport { .. } | Self::Timeout { .. } => true,
136            Self::Rejected { retryable, .. } => *retryable,
137            // A capability mismatch is a deployment fact, not a transient
138            // condition: retrying the same pair of DID documents cannot help.
139            // Nor can it help a registry that answers the wrong question or a
140            // referral that does not close: both are stable facts about the
141            // deployment, and retrying only re-asks a question already
142            // answered wrongly.
143            Self::Config(_)
144            | Self::Contract(_)
145            | Self::NoMatchingTransport { .. }
146            | Self::AnswerMismatch { .. }
147            | Self::ReferralNotClosed { .. } => false,
148        }
149    }
150}