1/// Some helpers for structured error handling.
2///
3/// The helpers defined here allow you to pass type-safe error codes from
4/// the collab server to the client; and provide a mechanism for additional
5/// structured data alongside the message.
6///
7/// When returning an error, it can be as simple as:
8///
9/// `return Err(Error::Forbidden.into())`
10///
11/// If you'd like to log more context, you can set a message. These messages
12/// show up in our logs, but are not shown visibly to users.
13///
14/// `return Err(Error::Forbidden.message("not an admin").into())`
15///
16/// If you'd like to provide enough context that the UI can render a good error
17/// message (or would be helpful to see in a structured format in the logs), you
18/// can use .with_tag():
19///
20/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())`
21///
22/// When handling an error you can use .error_code() to match which error it was
23/// and .error_tag() to read any tags.
24///
25/// ```
26/// match err.error_code() {
27/// ErrorCode::Forbidden => alert("I'm sorry I can't do that.")
28/// ErrorCode::WrongReleaseChannel =>
29/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap()))
30/// ErrorCode::Internal => alert("Sorry, something went wrong")
31/// }
32/// ```
33///
34pub use crate::ErrorCode;
35
36/// ErrorCodeExt provides some helpers for structured error handling.
37///
38/// The primary implementation is on the proto::ErrorCode to easily convert
39/// that into an anyhow::Error, which we use pervasively.
40///
41/// The RpcError struct provides support for further metadata if needed.
42pub trait ErrorCodeExt {
43 /// Return an anyhow::Error containing this.
44 /// (useful in places where .into() doesn't have enough type information)
45 fn anyhow(self) -> anyhow::Error;
46
47 /// Add a message to the error (by default the error code is used)
48 fn message(self, msg: String) -> RpcError;
49
50 /// Add a tag to the error. Tags are key value pairs that can be used
51 /// to send semi-structured data along with the error.
52 fn with_tag(self, k: &str, v: &str) -> RpcError;
53}
54
55impl ErrorCodeExt for ErrorCode {
56 fn anyhow(self) -> anyhow::Error {
57 self.into()
58 }
59
60 fn message(self, msg: String) -> RpcError {
61 let err: RpcError = self.into();
62 err.message(msg)
63 }
64
65 fn with_tag(self, k: &str, v: &str) -> RpcError {
66 let err: RpcError = self.into();
67 err.with_tag(k, v)
68 }
69}
70
71/// ErrorExt provides helpers for structured error handling.
72///
73/// The primary implementation is on the anyhow::Error, which is
74/// what we use throughout our codebase. Though under the hood this
75pub trait ErrorExt {
76 /// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none)
77 fn error_code(&self) -> ErrorCode;
78 /// error_tag() returns the value of the tag with the given key, if any.
79 fn error_tag(&self, k: &str) -> Option<&str>;
80 /// to_proto() converts the error into a crate::Error
81 fn to_proto(&self) -> crate::Error;
82 /// Clones the error and turns into an [anyhow::Error].
83 fn cloned(&self) -> anyhow::Error;
84}
85
86impl ErrorExt for anyhow::Error {
87 fn error_code(&self) -> ErrorCode {
88 if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
89 rpc_error.code
90 } else {
91 ErrorCode::Internal
92 }
93 }
94
95 fn error_tag(&self, k: &str) -> Option<&str> {
96 if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
97 rpc_error.error_tag(k)
98 } else {
99 None
100 }
101 }
102
103 fn to_proto(&self) -> crate::Error {
104 if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
105 rpc_error.to_proto()
106 } else {
107 ErrorCode::Internal
108 .message(
109 format!("{self:#}")
110 .lines()
111 .fold(String::new(), |mut message, line| {
112 if !message.is_empty() {
113 message.push(' ');
114 }
115 message.push_str(line);
116 message
117 }),
118 )
119 .to_proto()
120 }
121 }
122
123 fn cloned(&self) -> anyhow::Error {
124 if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
125 rpc_error.cloned()
126 } else {
127 anyhow::anyhow!("{self}")
128 }
129 }
130}
131
132impl From<ErrorCode> for anyhow::Error {
133 fn from(value: ErrorCode) -> Self {
134 RpcError {
135 request: None,
136 code: value,
137 msg: format!("{:?}", value),
138 tags: Default::default(),
139 }
140 .into()
141 }
142}
143
144#[derive(Clone, Debug)]
145pub struct RpcError {
146 request: Option<String>,
147 msg: String,
148 code: ErrorCode,
149 tags: Vec<String>,
150}
151
152/// RpcError is a structured error type that is returned by the collab server.
153/// In addition to a message, it lets you set a specific ErrorCode, and attach
154/// small amounts of metadata to help the client handle the error appropriately.
155///
156/// This struct is not typically used directly, as we pass anyhow::Error around
157/// in the app; however it is useful for chaining .message() and .with_tag() on
158/// ErrorCode.
159impl RpcError {
160 /// from_proto converts a crate::Error into an anyhow::Error containing
161 /// an RpcError.
162 pub fn from_proto(error: &crate::Error, request: &str) -> anyhow::Error {
163 RpcError {
164 request: Some(request.to_string()),
165 code: error.code(),
166 msg: error.message.clone(),
167 tags: error.tags.clone(),
168 }
169 .into()
170 }
171}
172
173impl ErrorCodeExt for RpcError {
174 fn message(mut self, msg: String) -> RpcError {
175 self.msg = msg;
176 self
177 }
178
179 fn with_tag(mut self, k: &str, v: &str) -> RpcError {
180 self.tags.push(format!("{}={}", k, v));
181 self
182 }
183
184 fn anyhow(self) -> anyhow::Error {
185 self.into()
186 }
187}
188
189impl ErrorExt for RpcError {
190 fn error_tag(&self, k: &str) -> Option<&str> {
191 for tag in &self.tags {
192 let mut parts = tag.split('=');
193 if let Some(key) = parts.next()
194 && key == k
195 {
196 return parts.next();
197 }
198 }
199 None
200 }
201
202 fn error_code(&self) -> ErrorCode {
203 self.code
204 }
205
206 fn to_proto(&self) -> crate::Error {
207 crate::Error {
208 code: self.code as i32,
209 message: self.msg.clone(),
210 tags: self.tags.clone(),
211 }
212 }
213
214 fn cloned(&self) -> anyhow::Error {
215 self.clone().into()
216 }
217}
218
219impl std::error::Error for RpcError {
220 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
221 None
222 }
223}
224
225impl std::fmt::Display for RpcError {
226 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
227 if let Some(request) = &self.request {
228 write!(f, "RPC request {} failed: {}", request, self.msg)?
229 } else {
230 write!(f, "{}", self.msg)?
231 }
232 for tag in &self.tags {
233 write!(f, " {}", tag)?
234 }
235 Ok(())
236 }
237}
238
239impl From<ErrorCode> for RpcError {
240 fn from(code: ErrorCode) -> Self {
241 RpcError {
242 request: None,
243 code,
244 msg: format!("{:?}", code),
245 tags: Default::default(),
246 }
247 }
248}