From 789af0bbf208aae09a6a15d26d6f97a56cd5fbdf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 7 Jun 2018 18:53:27 +0200 Subject: [PATCH] Added improved failure interoperability with downcasting (#285) Deprecates Error::cause and introduces failure interoperability functions and downcasting. --- src/error.rs | 110 +++++++++++++++++++++++++++++++++++++++++--- src/httpresponse.rs | 2 +- 2 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/error.rs b/src/error.rs index cfb6a0287..d08093fb2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,21 +34,42 @@ use httpresponse::HttpResponse; /// `Result`. pub type Result = result::Result; -/// General purpose actix web error +/// General purpose actix web error. +/// +/// An actix web error is used to carry errors from `failure` or `std::error` +/// through actix in a convenient way. It can be created through through +/// converting errors with `into()`. +/// +/// Whenever it is created from an external object a response error is created +/// for it that can be used to create an http response from it this means that +/// if you have access to an actix `Error` you can always get a +/// `ResponseError` reference from it. pub struct Error { cause: Box, backtrace: Option, } impl Error { - /// Returns a reference to the underlying cause of this Error. - // this should return &Fail but needs this https://github.com/rust-lang/rust/issues/5665 + /// Deprecated way to reference the underlying response error. + #[deprecated(since = "0.6.0", note = "please use `Error::as_response_error()` instead")] pub fn cause(&self) -> &ResponseError { self.cause.as_ref() } + /// Returns a reference to the underlying cause of this `Error` as `Fail` + pub fn as_fail(&self) -> &Fail { + self.cause.as_fail() + } + + /// Returns the reference to the underlying `ResponseError`. + pub fn as_response_error(&self) -> &ResponseError { + self.cause.as_ref() + } + /// Returns a reference to the Backtrace carried by this error, if it /// carries one. + /// + /// This uses the same `Backtrace` type that `failure` uses. pub fn backtrace(&self) -> &Backtrace { if let Some(bt) = self.cause.backtrace() { bt @@ -56,10 +77,61 @@ impl Error { self.backtrace.as_ref().unwrap() } } + + /// Attempts to downcast this `Error` to a particular `Fail` type by reference. + /// + /// If the underlying error is not of type `T`, this will return `None`. + pub fn downcast_ref(&self) -> Option<&T> { + // in the most trivial way the cause is directly of the requested type. + if let Some(rv) = Fail::downcast_ref(self.cause.as_fail()) { + return Some(rv); + } + + // in the more complex case the error has been constructed from a failure + // error. This happens because we implement From by + // calling compat() and then storing it here. In failure this is + // represented by a failure::Error being wrapped in a failure::Compat. + // + // So we first downcast into that compat, to then further downcast through + // the failure's Error downcasting system into the original failure. + // + // This currently requires a transmute. This could be avoided if failure + // provides a deref: https://github.com/rust-lang-nursery/failure/pull/213 + let compat: Option<&failure::Compat> = Fail::downcast_ref(self.cause.as_fail()); + if let Some(compat) = compat { + pub struct CompatWrappedError { + error: failure::Error, + } + let compat: &CompatWrappedError = unsafe { + ::std::mem::transmute(compat) + }; + compat.error.downcast_ref() + } else { + None + } + } +} + +/// Helper trait to downcast a response error into a fail. +/// +/// This is currently not exposed because it's unclear if this is the best way to +/// achieve the downcasting on `Error` for which this is needed. +#[doc(hidden)] +pub trait InternalResponseErrorAsFail { + #[doc(hidden)] + fn as_fail(&self) -> &Fail; + #[doc(hidden)] + fn as_mut_fail(&mut self) -> &mut Fail; +} + +#[doc(hidden)] +impl InternalResponseErrorAsFail for T { + fn as_fail(&self) -> &Fail { self } + fn as_mut_fail(&mut self) -> &mut Fail { self } } /// Error that can be converted to `HttpResponse` -pub trait ResponseError: Fail { +pub trait ResponseError: Fail + InternalResponseErrorAsFail { /// Create response for error /// /// Internal server error is generated by default. @@ -853,7 +925,7 @@ mod tests { } #[test] - fn test_cause() { + fn test_as_fail() { let orig = io::Error::new(io::ErrorKind::Other, "other"); let desc = orig.description().to_owned(); let e = ParseError::Io(orig); @@ -871,7 +943,7 @@ mod tests { let orig = io::Error::new(io::ErrorKind::Other, "other"); let desc = orig.description().to_owned(); let e = Error::from(orig); - assert_eq!(format!("{}", e.cause()), desc); + assert_eq!(format!("{}", e.as_fail()), desc); } #[test] @@ -970,6 +1042,32 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + #[test] + fn test_error_downcasting_direct() { + #[derive(Debug, Fail)] + #[fail(display = "demo error")] + struct DemoError; + + impl ResponseError for DemoError {} + + let err: Error = DemoError.into(); + let err_ref: &DemoError = err.downcast_ref().unwrap(); + assert_eq!(err_ref.to_string(), "demo error"); + } + + #[test] + fn test_error_downcasting_compat() { + #[derive(Debug, Fail)] + #[fail(display = "demo error")] + struct DemoError; + + impl ResponseError for DemoError {} + + let err: Error = failure::Error::from(DemoError).into(); + let err_ref: &DemoError = err.downcast_ref().unwrap(); + assert_eq!(err_ref.to_string(), "demo error"); + } + #[test] fn test_error_helpers() { let r: HttpResponse = ErrorBadRequest("err").into(); diff --git a/src/httpresponse.rs b/src/httpresponse.rs index ac84b2be4..ce0360272 100644 --- a/src/httpresponse.rs +++ b/src/httpresponse.rs @@ -89,7 +89,7 @@ impl HttpResponse { /// Constructs an error response #[inline] pub fn from_error(error: Error) -> HttpResponse { - let mut resp = error.cause().error_response(); + let mut resp = error.as_response_error().error_response(); resp.get_mut().error = Some(error); resp }