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.message(format!("{}", self)).to_proto()
108 }
109 }
110
111 fn cloned(&self) -> anyhow::Error {
112 if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
113 rpc_error.cloned()
114 } else {
115 anyhow::anyhow!("{}", self)
116 }
117 }
118}
119
120impl From<ErrorCode> for anyhow::Error {
121 fn from(value: ErrorCode) -> Self {
122 RpcError {
123 request: None,
124 code: value,
125 msg: format!("{:?}", value).to_string(),
126 tags: Default::default(),
127 }
128 .into()
129 }
130}
131
132#[derive(Clone, Debug)]
133pub struct RpcError {
134 request: Option<String>,
135 msg: String,
136 code: ErrorCode,
137 tags: Vec<String>,
138}
139
140/// RpcError is a structured error type that is returned by the collab server.
141/// In addition to a message, it lets you set a specific ErrorCode, and attach
142/// small amounts of metadata to help the client handle the error appropriately.
143///
144/// This struct is not typically used directly, as we pass anyhow::Error around
145/// in the app; however it is useful for chaining .message() and .with_tag() on
146/// ErrorCode.
147impl RpcError {
148 /// from_proto converts a crate::Error into an anyhow::Error containing
149 /// an RpcError.
150 pub fn from_proto(error: &crate::Error, request: &str) -> anyhow::Error {
151 RpcError {
152 request: Some(request.to_string()),
153 code: error.code(),
154 msg: error.message.clone(),
155 tags: error.tags.clone(),
156 }
157 .into()
158 }
159}
160
161impl ErrorCodeExt for RpcError {
162 fn message(mut self, msg: String) -> RpcError {
163 self.msg = msg;
164 self
165 }
166
167 fn with_tag(mut self, k: &str, v: &str) -> RpcError {
168 self.tags.push(format!("{}={}", k, v));
169 self
170 }
171
172 fn anyhow(self) -> anyhow::Error {
173 self.into()
174 }
175}
176
177impl ErrorExt for RpcError {
178 fn error_tag(&self, k: &str) -> Option<&str> {
179 for tag in &self.tags {
180 let mut parts = tag.split('=');
181 if let Some(key) = parts.next() {
182 if key == k {
183 return parts.next();
184 }
185 }
186 }
187 None
188 }
189
190 fn error_code(&self) -> ErrorCode {
191 self.code
192 }
193
194 fn to_proto(&self) -> crate::Error {
195 crate::Error {
196 code: self.code as i32,
197 message: self.msg.clone(),
198 tags: self.tags.clone(),
199 }
200 }
201
202 fn cloned(&self) -> anyhow::Error {
203 self.clone().into()
204 }
205}
206
207impl std::error::Error for RpcError {
208 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
209 None
210 }
211}
212
213impl std::fmt::Display for RpcError {
214 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
215 if let Some(request) = &self.request {
216 write!(f, "RPC request {} failed: {}", request, self.msg)?
217 } else {
218 write!(f, "{}", self.msg)?
219 }
220 for tag in &self.tags {
221 write!(f, " {}", tag)?
222 }
223 Ok(())
224 }
225}
226
227impl From<ErrorCode> for RpcError {
228 fn from(code: ErrorCode) -> Self {
229 RpcError {
230 request: None,
231 code,
232 msg: format!("{:?}", code).to_string(),
233 tags: Default::default(),
234 }
235 }
236}