darkbio_wire/protocol/error.rs
1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Errors returned by protocol methods and promises.
8
9use super::schema;
10use crate::transport;
11use std::convert::Infallible;
12use std::sync::Arc;
13
14/// Failure of a protocol operation. A remote application's error is carried by
15/// [`Error::Remote`]; it does not by itself end the session.
16#[derive(Clone, Debug, thiserror::Error)]
17pub enum Error {
18 /// The session or server was closed locally, including by dropping its owner.
19 #[error("wire protocol closed")]
20 Closed,
21
22 /// The operation's absolute deadline expired. Remote work may still run.
23 #[error("wire operation timed out")]
24 Timeout,
25
26 /// The underlying transport failed or the peer reset the session.
27 #[error("wire transport failed: {0}")]
28 Transport(#[from] Arc<transport::Error>),
29
30 /// The peer returned an application error for this request.
31 #[error("wire peer failed the request, code {}: {}", .0.code, .0.msg)]
32 Remote(schema::Error),
33
34 /// The response's `Message` variant does not match the type requested by
35 /// `Promise::wait()`. This does not end the session.
36 #[error("wire response type mismatch: expected {expected}, received {received}")]
37 UnexpectedResponse {
38 /// Expected protobuf message type.
39 expected: &'static str,
40 /// Received protobuf message type.
41 received: &'static str,
42 },
43
44 /// The submitted message cannot be sent from this session's side. Reported
45 /// through the request or reply promise; this error does not end the session.
46 #[error("wire message cannot be sent in this direction: {0}")]
47 WrongDirection(&'static str),
48
49 /// The encoded message exceeds the transport's sending limit.
50 #[error("wire message too large: {0} bytes")]
51 TooLarge(usize),
52
53 /// The peer sent an invalid envelope or payload. The session closes when this
54 /// is detected. Nested payloads are checked only when `recv()` or `wait()`
55 /// reads them.
56 #[error("wire peer sent a malformed message")]
57 Malformed,
58
59 /// A peer request would exceed the session's request limit. Also returned if
60 /// the limit is lowered below usage. Carries the configured request limit.
61 /// This closes the session.
62 #[error("wire inbound request limit exceeded: {0}")]
63 InboundRequestLimitExceeded(usize),
64
65 /// Buffering an incoming envelope would exceed the session's byte limit.
66 /// Also returned if the limit is lowered below usage. Carries the configured
67 /// byte limit. This closes the session.
68 #[error("wire inbound byte limit exceeded: {0}")]
69 InboundByteLimitExceeded(usize),
70}
71
72impl From<transport::Error> for Error {
73 /// Wraps a transport error in an `Arc` so pending promises can share it.
74 fn from(error: transport::Error) -> Self {
75 Self::Transport(Arc::new(error))
76 }
77}
78
79impl From<schema::Error> for Error {
80 /// Wraps the peer's error code and message in `Error::Remote`.
81 fn from(error: schema::Error) -> Self {
82 Self::Remote(error)
83 }
84}
85
86impl From<Infallible> for Error {
87 /// Allows `Promise::wait()` to return `Message` without extracting a variant.
88 fn from(error: Infallible) -> Self {
89 match error {}
90 }
91}
92
93impl Error {
94 /// Whether a session or server ending with this error did so in an orderly
95 /// way, through a local close, a peer reset or the stream ending.
96 pub(super) fn orderly(&self) -> bool {
97 match self {
98 Self::Closed => true,
99 Self::Transport(error) => matches!(
100 **error,
101 transport::Error::SessionReset | transport::Error::Terminated
102 ),
103 _ => false,
104 }
105 }
106
107 /// The error as a log reason, a transport failure named by the transport's
108 /// own error rather than by the wrapping one.
109 pub(super) fn reason(&self) -> &dyn std::fmt::Display {
110 match self {
111 Self::Transport(error) => error.as_ref(),
112 other => other,
113 }
114 }
115}
116
117impl schema::Error {
118 /// Builds an error with a numeric code and a human-readable message.
119 /// Codes from 0x100 are request-specific. Use [`Self::reserved`] for named
120 /// protocol errors.
121 pub fn new(code: u64, msg: impl Into<String>) -> Self {
122 Self {
123 code,
124 msg: msg.into(),
125 }
126 }
127
128 /// Builds an error from a reserved protocol code and a human-readable message.
129 pub fn reserved(code: schema::ReservedErrors, msg: impl Into<String>) -> Self {
130 Self::new(code as u64, msg)
131 }
132}
133
134/// An application failure a request is answered with. The peer dispatches on
135/// the code and may show the message. Codes below 0x100 are the protocol's
136/// [`schema::ReservedErrors`], an application assigns its own from 0x100 up.
137///
138/// Implementing it converts the error into [`schema::Error`], so a handler can
139/// fail a request with `?` and [`super::Responder::fail`] takes it directly.
140pub trait CodedError: std::error::Error {
141 /// Code identifying the failure to the peer.
142 fn code(&self) -> u64;
143}
144
145impl<E: CodedError> From<E> for schema::Error {
146 /// Converts an application error into its wire form, the code as assigned
147 /// and the message as displayed.
148 fn from(error: E) -> Self {
149 debug_assert!(
150 error.code() >= 0x100,
151 "application error code in the reserved range"
152 );
153 Self::new(error.code(), error.to_string())
154 }
155}