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