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