188 lines
8 KiB
Rust
188 lines
8 KiB
Rust
use crate::http_api::upload::headers::UploadOffsetResponseHeader;
|
|
use crate::upload_manager::UploadFailureReason;
|
|
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> },
|
|
RequestBodyTooLong,
|
|
RequestBodyTooShort,
|
|
|
|
IanaUploadAlreadyComplete,
|
|
|
|
CaosUploadRequestSuperseded,
|
|
CaosUploadFailed { reason: UploadFailureReason },
|
|
CaosUploadOffsetMismatch { expected: u64, provided: u64 },
|
|
CaosInconsistentUploadLength { expected: u64, detail: Cow<'static, str> },
|
|
CaosUploadNotFinished,
|
|
CaosUnknownBucket { bucket_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(),
|
|
ApiError::RequestBodyTooLong => (
|
|
StatusCode::BAD_REQUEST,
|
|
ProblemJson(json!({
|
|
"type": "https://minna.media/api-problems/general/request-body-too-long",
|
|
"title": "The received request body is longer than expected.",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::RequestBodyTooShort => (
|
|
StatusCode::BAD_REQUEST,
|
|
ProblemJson(json!({
|
|
"type": "https://minna.media/api-problems/general/request-body-too-short",
|
|
"title": "The received request body is shorter than expected.",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::IanaUploadAlreadyComplete => (
|
|
StatusCode::CONFLICT,
|
|
// According to https://www.ietf.org/archive/id/draft-ietf-httpbis-resumable-upload-08.html#section-4.4.2-7,
|
|
// if the request did not (itself) complete the upload, the response must contain the `Upload-Complete: ?0` header.
|
|
// When the request is *already* complete, the request cannot ever possibly complete the upload.
|
|
ProblemJson(json!({
|
|
"type": "https://iana.org/assignments/http-problem-types#completed-upload",
|
|
"title": "The upload is already complete.",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::CaosUploadRequestSuperseded => (
|
|
StatusCode::CONFLICT,
|
|
ProblemJson(json!({
|
|
"type": "https://minna.media/api-problems/caos/request-superseded",
|
|
"title": "Another request superseded the current request.",
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::CaosUploadFailed { reason } => (
|
|
StatusCode::GONE,
|
|
ProblemJson(json!({
|
|
"type": "https://minna.media/api-problems/caos/request-superseded",
|
|
"title": "The upload was cancelled or failed.",
|
|
"reason": reason.to_string()
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::CaosUploadOffsetMismatch { expected, provided } => (
|
|
StatusCode::CONFLICT,
|
|
UploadOffsetResponseHeader(expected),
|
|
ProblemJson(json!({
|
|
"type": "https://iana.org/assignments/http-problem-types#mismatching-upload-offset",
|
|
"title": "The upload offset provided in the request does not match the actual offset of the resource.",
|
|
"expected-offset": expected,
|
|
"provided-offset": provided,
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::CaosInconsistentUploadLength { expected, detail } => (
|
|
StatusCode::CONFLICT,
|
|
UploadOffsetResponseHeader(expected),
|
|
ProblemJson(json!({
|
|
"type": "https://iana.org/assignments/http-problem-types#inconsistent-upload-length",
|
|
"title": "The provided upload lengths are inconsistent with one another or a previously established total length.",
|
|
"detail": detail,
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::CaosUploadNotFinished => (
|
|
StatusCode::CONFLICT,
|
|
ProblemJson(json!({
|
|
"type": "https://minna.media/api-problems/caos/upload-not-finished",
|
|
"title": "The upload is not finished yet."
|
|
})),
|
|
)
|
|
.into_response(),
|
|
ApiError::CaosUnknownBucket { bucket_id } => (
|
|
StatusCode::CONFLICT,
|
|
ProblemJson(json!({
|
|
"type": "https://minna.media/api-problems/caos/unknown-bucket",
|
|
"title": "There is no bucket with the specified ID.",
|
|
"bucketId": bucket_id
|
|
})),
|
|
)
|
|
.into_response(),
|
|
}
|
|
}
|
|
}
|