#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Problem {
pub status: u16,
pub title: &'static str,
pub detail: String,
}
impl Problem {
pub fn new(status: u16, title: &'static str, detail: impl Into<String>) -> Self {
Self {
status,
title,
detail: detail.into(),
}
}
pub fn json(&self) -> String {
format!(
r#"{{"type":"about:blank","title":"{}","status":{},"detail":"{}"}}"#,
json_escape(self.title),
self.status,
json_escape(&self.detail)
)
}
}
pub(crate) fn json_escape(text: &str) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
c if c < ' ' => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}
pub trait ExitError: std::error::Error {
fn exit_code(&self) -> i32;
fn problem(&self) -> Problem;
}
pub fn run<E: ExitError>(main: impl FnOnce() -> Result<(), E>) {
if let Err(error) = main() {
eprintln!("error: {}", error.problem().detail);
std::process::exit(error.exit_code());
}
}
pub fn run_json<E: ExitError>(main: impl FnOnce() -> Result<(), E>) {
if let Err(error) = main() {
eprintln!("{}", error.problem().json());
std::process::exit(error.exit_code());
}
}
#[cfg(feature = "config")]
impl ExitError for crate::config::ConfigError {
fn exit_code(&self) -> i32 {
78
}
fn problem(&self) -> Problem {
Problem::new(500, "Configuration error", self.to_string())
}
}
#[cfg(feature = "db")]
impl ExitError for crate::db::DbError {
fn exit_code(&self) -> i32 {
match self {
Self::Engine(_) => 78,
Self::Open { .. } => 74,
}
}
fn problem(&self) -> Problem {
match self {
Self::Engine(_) => Problem::new(500, "Configuration error", self.to_string()),
Self::Open { .. } => Problem::new(503, "Database unavailable", self.to_string()),
}
}
}
#[cfg(feature = "web")]
impl ExitError for crate::web::ServeError {
fn exit_code(&self) -> i32 {
match self {
Self::Config(error) => error.exit_code(),
Self::Bind { .. } => 69,
}
}
fn problem(&self) -> Problem {
match self {
Self::Config(error) => error.problem(),
Self::Bind { .. } => Problem::new(503, "Port unavailable", self.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn problem_json_has_the_http_error_shape_and_escapes() {
assert_eq!(
Problem::new(503, "Down", "say \"hi\"\n").json(),
r#"{"type":"about:blank","title":"Down","status":503,"detail":"say \"hi\"\u000a"}"#
);
}
#[cfg(feature = "web")]
#[test]
fn framework_errors_answer_with_codes_and_problems() {
use crate::config::ConfigError;
use crate::web::ServeError;
let missing = ConfigError::Missing("PORT".into());
assert_eq!((missing.exit_code(), missing.problem().status), (78, 500));
let serve = ServeError::Config(missing.clone());
assert_eq!(serve.exit_code(), 78);
assert_eq!(serve.problem(), missing.problem());
let busy = ServeError::Bind {
port: 80,
source: std::io::ErrorKind::AddrInUse.into(),
};
assert_eq!((busy.exit_code(), busy.problem().status), (69, 503));
assert!(busy.problem().detail.contains("Port 80"));
}
#[cfg(feature = "db")]
#[test]
fn database_errors_split_config_from_io() {
use crate::db::DbError;
let engine = DbError::Engine("mysql".into());
assert_eq!(engine.exit_code(), 78);
assert!(engine.problem().detail.contains("mysql"));
}
}