error.rs

  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}