Skip to main content

protocol_droid/application/server/
error.rs

1//! Errors the server API returns before anything reaches the wire.
2
3use crate::domain::ProtocolVersion;
4use crate::domain::feature::Feature;
5
6/// Why a feature-specific call was refused.
7///
8/// Every variant here is a refusal the peer never sees. The distinction from a
9/// JSON-RPC error matters: an error the peer sees is a protocol interaction,
10/// and this crate is careful about when it sends one. A handler that asks for
11/// a capability the session does not have is not a protocol error — the request
12/// was well formed and the server simply cannot serve it the way the handler
13/// imagined. Sending a JSON-RPC error there teaches the peer something false
14/// about the protocol, so the failure stays on this side of the wire and the
15/// handler is told directly.
16///
17/// That is the whole reason this type exists rather than reusing the
18/// lower-time `LowerError`: one happens during a translation the peer asked
19/// for, the other happens before anything is written at all.
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum GatedError {
22    /// The negotiated revision and the peer's declared capabilities do not
23    /// include the feature the call needs.
24    ///
25    /// The negotiated version is carried rather than left to the caller to
26    /// remember, because "elicitation is unavailable" is an answer nobody can
27    /// act on without knowing what *was* negotiated.
28    #[error("{feature} was not negotiated (session is {negotiated})")]
29    FeatureUnavailable {
30        /// The feature the call needed.
31        feature: Feature,
32        /// The revision this session actually settled on.
33        negotiated: ProtocolVersion,
34    },
35
36    /// A progress report was attempted on a request that was never given a
37    /// progress token.
38    ///
39    /// Separate from [`FeatureUnavailable`](Self::FeatureUnavailable) because
40    /// the fix is different: progress is available on every revision, and the
41    /// client is the one that declined to open a channel.
42    #[error("this request has no progress token; the client did not open one")]
43    NoProgressToken,
44
45    /// A `requestState` was to be sealed and the server holds no key.
46    ///
47    /// A server built in-process with no key, serving through no transport
48    /// that would have supplied one. Sealing under a placeholder would hand
49    /// out a state any client could forge, which is worse than refusing.
50    #[error("this server has no request-state key to seal with")]
51    NoStateKey,
52
53    /// The peer's answer to a server-initiated request did not arrive as JSON,
54    /// or the exchange did not complete.
55    ///
56    /// The gate is not implicated — the call was permitted, and the failure is
57    /// the peer's or the transport's. Kept here so a handler has one error
58    /// type to match rather than a gate error and a transport error.
59    #[error("the exchange did not complete: {reason}")]
60    MalformedAnswer {
61        /// What the transport reported.
62        ///
63        /// A `String` because the transport names its own failures and the
64        /// application layer has no business enumerating them. The rule is that
65        /// application code never names a _third-party_ error type; a
66        /// transport's own description is data the adapter chose to hand over.
67        reason: String,
68    },
69}
70
71impl GatedError {
72    /// The feature this refusal is about, when there is one.
73    ///
74    /// A progress-token refusal is about no feature, which is why this returns
75    /// an `Option` rather than inventing one — a caller logging the answer
76    /// wants to omit the field, not print a placeholder that reads as a claim.
77    #[must_use]
78    pub const fn feature(&self) -> Option<Feature> {
79        match self {
80            Self::FeatureUnavailable { feature, .. } => Some(*feature),
81            Self::NoProgressToken | Self::NoStateKey | Self::MalformedAnswer { .. } => None,
82        }
83    }
84
85    /// Whether nothing was written to the transport before this refusal.
86    ///
87    /// Always true, and that is the point: the gate runs before the exchange,
88    /// so a refused call cannot have left a partial frame behind. The
89    /// [`MalformedAnswer`](Self::MalformedAnswer) case is the exception in
90    /// spirit — the request _was_ written, because the gate had passed and the
91    /// failure came afterwards — so it reports false.
92    #[must_use]
93    pub const fn wrote_nothing(&self) -> bool {
94        match self {
95            Self::FeatureUnavailable { .. } | Self::NoProgressToken | Self::NoStateKey => true,
96            Self::MalformedAnswer { .. } => false,
97        }
98    }
99}