//! Typed API errors and their HTTP representation.
use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use twice_core::{ApiErrorBody, Host};
/// Everything a handler can fail with.
#[derive(Debug, thiserror::Error)]
pub(crate) enum ApiError {
/// The `x-api-key` header was missing, malformed, or wrong.
#[error("missing or invalid API key")]
Unauthorized,
/// The requested host is not a known application.
#[error("no application known for host {0}")]
UnknownApp(Host),
/// The `{host}` path segment was not a valid hostname.
#[error("invalid host {0:?}: {1}")]
BadHost(String, twice_core::InvalidHost),
/// The referenced job does not exist.
#[error("no such job")]
UnknownJob,
/// Another job is already running against the same application.
#[error("a {0} job is already running for this application")]
JobInFlight(&'static str),
/// The Docker daemon could not be queried.
#[error("docker daemon error: {0}")]
Docker(#[from] bollard::errors::Error),
/// The `once` binary could not be spawned.
#[error("could not run the once binary: {0}")]
Spawn(#[from] std::io::Error),
}
impl ApiError {
const fn parts(&self) -> (StatusCode, &'static str) {
match self {
Self::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized"),
Self::UnknownApp(_) => (StatusCode::NOT_FOUND, "unknown_app"),
Self::BadHost(_, _) => (StatusCode::BAD_REQUEST, "invalid_host"),
Self::UnknownJob => (StatusCode::NOT_FOUND, "unknown_job"),
Self::JobInFlight(_) => (StatusCode::CONFLICT, "job_in_flight"),
Self::Docker(_) => (StatusCode::BAD_GATEWAY, "docker_unavailable"),
Self::Spawn(_) => (StatusCode::INTERNAL_SERVER_ERROR, "spawn_failed"),
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, code) = self.parts();
let body = ApiErrorBody {
error: code.to_owned(),
message: self.to_string(),
};
(status, Json(body)).into_response()
}
}