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/// ```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}