use std::{convert::Infallible, error::Error, fmt};
use bytes::Bytes;
use http::{HeaderValue, Response, StatusCode, header};
use crate::{
Categorized, Category, EmptyRange, ExecuteError, LoadError, NotARevision, NotCaughtUp,
PreconditionsError, ReadError, ReadOptionsError, RegisterSchemasError, Transient, WriteError,
};
#[cfg(test)]
mod tests;
type Cause = Box<dyn Error + Send + Sync + 'static>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Logged {
Not,
AsRefusal,
AsFailure,
}
#[derive(Debug)]
pub struct HttpError {
status: StatusCode,
message: String,
cause: Option<Cause>,
logged: Logged,
}
impl HttpError {
pub fn unauthorized(cause: impl Into<Cause>) -> Self {
Self::refused(StatusCode::UNAUTHORIZED, "unauthorized", cause.into())
}
pub fn forbidden(reason: impl fmt::Display) -> Self {
Self::told(StatusCode::FORBIDDEN, format!("forbidden: {reason}"))
}
pub fn not_found(what: impl fmt::Display) -> Self {
Self::told(StatusCode::NOT_FOUND, format!("not found: {what}"))
}
pub fn malformed(reason: impl fmt::Display) -> Self {
Self::told(
StatusCode::BAD_REQUEST,
format!("malformed request: {reason}"),
)
}
pub fn too_large(reason: impl fmt::Display) -> Self {
Self::told(
StatusCode::PAYLOAD_TOO_LARGE,
format!("request body too large: {reason}"),
)
}
pub fn unsupported_media_type(reason: impl fmt::Display) -> Self {
Self::told(
StatusCode::UNSUPPORTED_MEDIA_TYPE,
format!("unsupported media type: {reason}"),
)
}
pub fn of<E: Error + Categorized + Send + Sync + 'static>(error: E) -> Self {
match error.category() {
Some(Category::Domain) => {
let message = error.to_string();
Self {
status: StatusCode::UNPROCESSABLE_ENTITY,
message,
cause: Some(Box::new(error)),
logged: Logged::Not,
}
}
Some(Category::Transient) => Self::failed(
StatusCode::SERVICE_UNAVAILABLE,
"internal server error",
Box::new(error),
),
Some(Category::Permanent) | None => Self::failed(
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error",
Box::new(error),
),
}
}
#[must_use]
pub fn status(&self) -> StatusCode {
self.status
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
fn told(status: StatusCode, message: String) -> Self {
Self {
status,
message,
cause: None,
logged: Logged::Not,
}
}
fn refused(status: StatusCode, message: &str, cause: Cause) -> Self {
Self {
status,
message: message.to_owned(),
cause: Some(cause),
logged: Logged::AsRefusal,
}
}
fn failed(status: StatusCode, message: &str, cause: Cause) -> Self {
Self {
status,
message: message.to_owned(),
cause: Some(cause),
logged: Logged::AsFailure,
}
}
fn log(&self) {
let cause = Chain(self.cause.as_deref());
match self.logged {
Logged::Not => {}
Logged::AsRefusal => log::info!(
"architecturekit: request refused with {}: {cause}",
self.status.as_u16(),
),
Logged::AsFailure => log::error!(
"architecturekit: internal failure, answered with {}: {cause}",
self.status.as_u16(),
),
}
}
}
impl fmt::Display for HttpError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl Error for HttpError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
let cause: &(dyn Error + 'static) = self.cause.as_deref()?;
Some(cause)
}
}
impl From<HttpError> for Response<Bytes> {
fn from(error: HttpError) -> Self {
error.log();
let body = format!("{}\n", serde_json::json!({ "message": error.message }));
let mut response = Response::new(Bytes::from(body));
*response.status_mut() = error.status;
let headers = response.headers_mut();
let _ = headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let _ = headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
response
}
}
#[cfg(feature = "axum")]
impl axum::response::IntoResponse for HttpError {
fn into_response(self) -> axum::response::Response {
Response::<Bytes>::from(self).map(axum::body::Body::from)
}
}
impl<D: Error + Categorized + Send + Sync + 'static> From<crate::Error<D>> for HttpError {
fn from(error: crate::Error<D>) -> Self {
match error {
crate::Error::Decider(error) => Self::of(error),
crate::Error::Transient(Transient::Conflict(failure)) => Self::refused(
StatusCode::CONFLICT,
"conflict: the data has changed since it was read",
Box::new(Transient::Conflict(failure)),
),
crate::Error::Transient(failure) => Self::of(failure),
crate::Error::Permanent(failure) => Self::of(failure),
error @ crate::Error::OutcomeUnknown(_) => Self::failed(
StatusCode::INTERNAL_SERVER_ERROR,
"outcome unknown: the request may have succeeded",
Box::new(error),
),
crate::Error::NotARevision(refusal) => refusal.into(),
crate::Error::EmptyRange(refusal) => refusal.into(),
}
}
}
impl<D: Error + Categorized + Send + Sync + 'static> From<ExecuteError<D>> for HttpError {
fn from(error: ExecuteError<D>) -> Self {
crate::Error::from(error).into()
}
}
macro_rules! through_the_failure_of_the_kit {
($($failure:ty),*) => {$(
impl From<$failure> for HttpError {
fn from(error: $failure) -> Self {
crate::Error::<Infallible>::from(error).into()
}
}
)*};
}
through_the_failure_of_the_kit!(
LoadError,
WriteError,
RegisterSchemasError,
ReadError,
ReadOptionsError,
PreconditionsError
);
impl From<NotARevision> for HttpError {
fn from(refusal: NotARevision) -> Self {
Self::told(StatusCode::BAD_REQUEST, refusal.to_string())
}
}
impl From<EmptyRange> for HttpError {
fn from(refusal: EmptyRange) -> Self {
Self::told(StatusCode::BAD_REQUEST, refusal.to_string())
}
}
impl<E: Error + Send + Sync + 'static> From<NotCaughtUp<E>> for HttpError {
fn from(error: NotCaughtUp<E>) -> Self {
Self::failed(
StatusCode::INTERNAL_SERVER_ERROR,
"the request succeeded, but its result is not visible yet",
Box::new(error),
)
}
}
impl From<Infallible> for HttpError {
fn from(never: Infallible) -> Self {
match never {}
}
}
struct Chain<'e>(Option<&'e (dyn Error + Send + Sync + 'static)>);
impl fmt::Display for Chain<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(error) = self.0 else {
return formatter.write_str("no cause");
};
write!(formatter, "{error}")?;
let mut source = error.source();
while let Some(cause) = source {
write!(formatter, ": {cause}")?;
source = cause.source();
}
Ok(())
}
}