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