minna-caos/src/http_api/api_error.rs
2025-04-17 00:53:56 +02:00

94 lines
3.5 KiB
Rust

use axum::Json;
use axum::http::{HeaderName, HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use color_eyre::Report;
use serde::Serialize;
use serde_json::json;
use std::borrow::Cow;
use tokio_util::bytes::{BufMut, BytesMut};
pub struct ProblemJson<T>(pub T);
impl<T: Serialize> IntoResponse for ProblemJson<T> {
fn into_response(self) -> Response {
// Same as IntoResponse::into_response for Json, but the content type header is changed.
let mut buf = BytesMut::with_capacity(128).writer();
match serde_json::to_writer(&mut buf, &self.0) {
Ok(()) => (
[(header::CONTENT_TYPE, HeaderValue::from_static("application/problem+json"))],
buf.into_inner().freeze(),
)
.into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
[(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"))],
err.to_string(),
)
.into_response(),
}
}
}
#[derive(Debug)]
pub enum ApiError {
Internal { report: Report },
Forbidden,
InvalidRequestHeader { name: HeaderName, message: Cow<'static, str> },
InvalidRequestContent { path: Cow<'static, str>, message: Cow<'static, str> },
UnknownResource { resource_type: Cow<'static, str>, id: Cow<'static, str> },
}
impl From<Report> for ApiError {
fn from(report: Report) -> Self {
ApiError::Internal { report }
}
}
impl From<std::io::Error> for ApiError {
fn from(error: std::io::Error) -> Self {
ApiError::Internal { report: Report::new(error) }
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::Internal { report } => {
log::error!("Internal error in request handler: {:#}", report);
StatusCode::INTERNAL_SERVER_ERROR.into_response()
}
ApiError::Forbidden => StatusCode::FORBIDDEN.into_response(),
ApiError::InvalidRequestHeader { name, message } => (
StatusCode::UNPROCESSABLE_ENTITY,
ProblemJson(json!({
"type": "https://minna.media/api-problems/general/invalid-request-header",
"title": "A specific request header value is invalid.",
"detail": format!("The value of `{}` is invalid: {}", name.as_str(), message)
})),
)
.into_response(),
ApiError::InvalidRequestContent { path, message } => (
StatusCode::UNPROCESSABLE_ENTITY,
ProblemJson(json!({
"type": "https://minna.media/api-problems/general/invalid-request-content",
"title": "The request content is semantically invalid.",
"detail": format!("`{path}`: {message}"),
"path": path
})),
)
.into_response(),
ApiError::UnknownResource { resource_type, id } => (
StatusCode::NOT_FOUND,
ProblemJson(json!({
"type": "https://minna.media/api-problems/general/unknown-resource",
"title": "The requested resource is unknown.",
"detail": format!("There is no {resource_type} resource with this ID: {id}"),
"resource_type": resource_type,
"resource_id": id
})),
)
.into_response(),
}
}
}