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