summaryrefslogtreecommitdiff
path: root/src/error
diff options
context:
space:
mode:
Diffstat (limited to 'src/error')
-rw-r--r--src/error/chain.rs72
-rw-r--r--src/error/failed.rs155
-rw-r--r--src/error/mod.rs79
3 files changed, 306 insertions, 0 deletions
diff --git a/src/error/chain.rs b/src/error/chain.rs
new file mode 100644
index 0000000..d78fe67
--- /dev/null
+++ b/src/error/chain.rs
@@ -0,0 +1,72 @@
+use std::{error::Error, io};
+
+use super::Id;
+
+pub fn format<W, E>(out: &mut W, error: E) -> Result<(), io::Error>
+where
+ W: io::Write,
+ E: Error,
+{
+ writeln!(out, "{error}")?;
+ format_sources(out, error)?;
+
+ Ok(())
+}
+
+pub fn format_with_id<W, E>(out: &mut W, id: &Id, error: E) -> Result<(), io::Error>
+where
+ W: io::Write,
+ E: Error,
+{
+ writeln!(out, "[{id}] {error}")?;
+ format_sources(out, error)?;
+
+ Ok(())
+}
+
+fn format_sources<W, E>(out: &mut W, error: E) -> Result<(), io::Error>
+where
+ W: io::Write,
+ E: Error,
+{
+ let mut sources = Sources::from(&error);
+ if let Some(source) = sources.next() {
+ writeln!(out)?;
+ writeln!(out, "Caused by:")?;
+ writeln!(out, " {source}")?;
+ for source in sources {
+ writeln!(out, " {source}")?;
+ }
+ writeln!(out)?;
+ }
+ Ok(())
+}
+
+struct Sources<'e> {
+ next: Option<&'e dyn Error>,
+}
+
+impl<'e, E> From<&'e E> for Sources<'e>
+where
+ E: Error,
+{
+ fn from(error: &'e E) -> Self {
+ Self {
+ next: error.source(),
+ }
+ }
+}
+
+// See also: <https://doc.rust-lang.org/std/error/trait.Error.html#method.sources> However, we only
+// want to iterate the sources, and not the error itself. Personally, I find the `skip(1)`
+// suggestion untidy, since the error itself is non-optional while the sources are optional.
+impl<'a> Iterator for Sources<'a> {
+ type Item = &'a dyn Error;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let source = self.next;
+ self.next = self.next.and_then(|err| err.source());
+
+ source
+ }
+}
diff --git a/src/error/failed.rs b/src/error/failed.rs
new file mode 100644
index 0000000..4d55552
--- /dev/null
+++ b/src/error/failed.rs
@@ -0,0 +1,155 @@
+use std::{borrow::Cow, error::Error, fmt};
+
+use super::BoxedError;
+
+// Don't bother allocating a String for each error if we have a static error message, which is
+// a fairly common use case.
+pub type Message = Cow<'static, str>;
+
+// Intended to represent a generic failure caused by some underlying cause, tagged with a
+// context-appropriate message to help operators and developers diagnose and repair the issue.
+// Unlike `Internal`, this error type has no default HTTP representation - it's not meant for that
+// use case: the intended audience for a `Failed` and its underlying cause is the service operator,
+// not the conversational users or client developers.
+//
+// The underlying cause will be used as the `Failed` error's `source()`, and the provided message
+// will be used as its `Display` implementation.
+//
+// Ways to make a `Failed`:
+//
+// * Call `ResultExt::fail` or `ResultExt::fail_with` on a result to convert its error type (most
+// preferred).
+// * Call `ErrorExt::fail` to convert an existing error into `Failed`.
+// * Call `Result::map_err` with `Failed::with_message` or `Failed::with_message_from` to convert
+// its error type to `Failed`.
+// * Call `Failed::new` with a message and an existing error (least preferred).
+//
+// Ways to use `Failed`:
+//
+// * As the error type of a `Result` return value, when all errors that can arise in a function
+// are of interest to the operator only but there are multiple failure types to contend with.
+// * As the error type of a `Result` return value, when there's only one error that can arise but
+// you want to annotate that error with a contextual message, like what the function was doing at
+// the time.
+// * As a variant in am error enum, when _some_ errors are only of operator interest but other
+// errors may need to be broken out for matching, or returned to clients.
+//
+// When not to use `Failed`:
+//
+// * When the caller needs to match on the specific error type. `Failed`, by design, assumes that
+// nothing up the call stack will care about the structure of the error, and while you _can_ get
+// at the underlying error to check what you ahve by going through the `source()` chain, it's
+// tricky to do well. If the specific error is relevant to the caller, surface it through the
+// error type, instead.
+#[derive(Debug)]
+pub struct Failed {
+ source: BoxedError,
+ message: Message,
+}
+
+impl Failed {
+ pub fn new<M, E>(message: M, source: E) -> Self
+ where
+ M: Into<Message>,
+ E: Into<BoxedError>,
+ {
+ Self {
+ source: source.into(),
+ message: message.into(),
+ }
+ }
+}
+
+// Adapters for use with `Result::map_err`.
+impl Failed {
+ // The returned adaptor wraps an error with `Failed`, using the provided message.
+ pub fn with_message<M, E>(message: M) -> impl FnOnce(E) -> Self
+ where
+ M: Into<Message>,
+ E: Into<BoxedError>,
+ {
+ |source| Self::new(message, source)
+ }
+
+ // The returned adaptor wraps an error with `Failed`, using a message from the provided message
+ // callback.
+ pub fn with_message_from<F, M, E>(message_fn: F) -> impl FnOnce(E) -> Self
+ where
+ F: FnOnce() -> M,
+ M: Into<Message>,
+ E: Into<BoxedError>,
+ {
+ |source| Self::new(message_fn(), source)
+ }
+}
+
+impl Error for Failed {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ Some(self.source.as_ref())
+ }
+}
+
+impl fmt::Display for Failed {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.message.fmt(f)
+ }
+}
+
+pub trait ErrorExt {
+ // Convenience for `Failed::new(message, err).into()`, when wrapping an existing error into
+ // `Failed`.
+ fn fail<M, T>(self, message: M) -> T
+ where
+ M: Into<Message>,
+ T: From<Failed>;
+}
+
+impl<E> ErrorExt for E
+where
+ E: Into<BoxedError>,
+{
+ fn fail<M, T>(self, message: M) -> T
+ where
+ M: Into<Message>,
+ T: From<Failed>,
+ {
+ Failed::new(message, self).into()
+ }
+}
+
+pub trait ResultExt {
+ type Ok;
+
+ // Convenience for `.map_err(Failed::with_message(message))`, when a result's error type can be
+ // collapsed into `Failed`.
+ fn fail<M>(self, message: M) -> Result<Self::Ok, Failed>
+ where
+ M: Into<Message>;
+
+ fn fail_with<F, M>(self, message_fn: F) -> Result<Self::Ok, Failed>
+ where
+ F: FnOnce() -> M,
+ M: Into<Message>;
+}
+
+impl<T, E> ResultExt for Result<T, E>
+where
+ E: Into<BoxedError>,
+{
+ type Ok = T;
+
+ fn fail<M>(self, message: M) -> Result<T, Failed>
+ where
+ M: Into<Message>,
+ {
+ self.map_err(Failed::with_message(message))
+ }
+
+ fn fail_with<F, M>(self, message_fn: F) -> Result<T, Failed>
+ where
+ F: FnOnce() -> M,
+ M: Into<Message>,
+ {
+ self.map_err(Failed::with_message_from(message_fn))
+ }
+}
diff --git a/src/error/mod.rs b/src/error/mod.rs
new file mode 100644
index 0000000..606e344
--- /dev/null
+++ b/src/error/mod.rs
@@ -0,0 +1,79 @@
+use std::{error, fmt, io};
+
+use axum::{
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+
+pub mod chain;
+pub mod failed;
+
+// I'm making an effort to avoid `anyhow` here, as that crate is _enormously_
+// complex (though very usable). We don't need to be overly careful about
+// allocations on errors in this app, so this is fine for most "general
+// failure" cases.
+type BoxedError = Box<dyn error::Error + Send + Sync>;
+
+// Returns a 500 Internal Server Error to the client. Meant to be used via the
+// `?` operator; _does not_ return the originating error to the client.
+#[derive(Debug)]
+pub struct Internal(Id, BoxedError);
+
+impl<E> From<E> for Internal
+where
+ E: Into<BoxedError>,
+{
+ fn from(error: E) -> Self {
+ let id = Id::generate();
+ Self(id, error.into())
+ }
+}
+
+impl fmt::Display for Internal {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let Self(id, _) = self;
+ writeln!(f, "internal server error")?;
+ writeln!(f, "error id: {id}")?;
+ Ok(())
+ }
+}
+
+impl IntoResponse for Internal {
+ fn into_response(self) -> Response {
+ let Self(id, error) = &self;
+ chain::format_with_id(&mut io::stderr().lock(), id, error.as_ref())
+ .expect("write to stderr");
+ (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
+ }
+}
+
+pub type Id = crate::id::Id<InternalError>;
+
+#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct InternalError;
+
+impl crate::id::Prefix for InternalError {
+ fn prefix(&self) -> &'static str {
+ "E"
+ }
+}
+
+pub struct Unauthorized;
+
+impl IntoResponse for Unauthorized {
+ fn into_response(self) -> Response {
+ (StatusCode::UNAUTHORIZED, "unauthorized").into_response()
+ }
+}
+
+pub struct NotFound<E>(pub E);
+
+impl<E> IntoResponse for NotFound<E>
+where
+ E: std::error::Error,
+{
+ fn into_response(self) -> Response {
+ let Self(response) = self;
+ (StatusCode::NOT_FOUND, response.to_string()).into_response()
+ }
+}