94 lines
3.5 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|