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/mod.rs78
2 files changed, 150 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/mod.rs b/src/error/mod.rs
new file mode 100644
index 0000000..154f98f
--- /dev/null
+++ b/src/error/mod.rs
@@ -0,0 +1,78 @@
+use std::{error, fmt, io};
+
+use axum::{
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+
+pub mod chain;
+
+// 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()
+ }
+}