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}