use std::fmt::Display;
use axum::body::Body;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use tracing::error;
use crate::domain::storage::errors::BucketStorageError;
#[derive(Debug, Clone, Copy, strum::Display)]
#[allow(clippy::upper_case_acronyms)]
#[non_exhaustive]
pub enum S3ErrorCodeKind {
BucketAlreadyExists,
InvalidBucketName,
InternalError,
InvalidURI,
KeyTooLongError,
InvalidRequest,
MalformedXML,
NoSuchBucket,
NoSuchKey,
}
impl S3ErrorCodeKind {
const fn status_code(&self) -> StatusCode {
match self {
S3ErrorCodeKind::BucketAlreadyExists => StatusCode::CONFLICT,
S3ErrorCodeKind::InvalidBucketName => StatusCode::BAD_REQUEST,
S3ErrorCodeKind::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
S3ErrorCodeKind::InvalidRequest => StatusCode::BAD_REQUEST,
S3ErrorCodeKind::InvalidURI => StatusCode::BAD_REQUEST,
S3ErrorCodeKind::KeyTooLongError => StatusCode::BAD_REQUEST,
S3ErrorCodeKind::MalformedXML => StatusCode::BAD_REQUEST,
S3ErrorCodeKind::NoSuchBucket => StatusCode::NOT_FOUND,
S3ErrorCodeKind::NoSuchKey => StatusCode::NOT_FOUND,
}
}
const fn message(&self) -> &'static str {
match self {
S3ErrorCodeKind::BucketAlreadyExists => {
"The requested bucket name is not available. The bucket \
namespace is shared by all users of the system. Please select \
a different name and try again."
}
S3ErrorCodeKind::InvalidBucketName => {
"The specified bucket is not valid."
}
S3ErrorCodeKind::InternalError => {
"An internal error occurred. Try again."
}
S3ErrorCodeKind::InvalidRequest => "Invalid Request",
S3ErrorCodeKind::InvalidURI => "Couldn't parse the specified URI.",
S3ErrorCodeKind::KeyTooLongError => "Your key is too long",
S3ErrorCodeKind::MalformedXML => {
"The XML that you provided was not well formed or did not \
validate against our published schema."
}
S3ErrorCodeKind::NoSuchBucket => {
"The specified bucket does not exist."
}
S3ErrorCodeKind::NoSuchKey => "The specified key does not exist.",
}
}
}
#[derive(Debug)]
pub struct S3Error {
message: Option<String>,
kind: S3ErrorCodeKind,
}
impl Display for S3Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.kind.fmt(f)
}
}
impl S3Error {
fn message(&self) -> &str {
if let Some(msg) = &self.message {
msg
} else {
self.kind.message()
}
}
const fn status_code(&self) -> StatusCode {
self.kind.status_code()
}
pub fn invalid_request(reason: &'static str) -> Self {
Self {
kind: S3ErrorCodeKind::InvalidRequest,
message: Some(reason.to_string()),
}
}
}
impl From<S3ErrorCodeKind> for S3Error {
fn from(value: S3ErrorCodeKind) -> Self {
Self {
kind: value,
message: None,
}
}
}
#[derive(Debug)]
pub struct S3HTTPError {
ressource: String,
request_id: String,
kind: Box<S3Error>,
}
impl Display for S3HTTPError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.kind.fmt(f)
}
}
impl std::error::Error for S3HTTPError {}
impl S3HTTPError {
pub fn custom<S1: AsRef<str>, S2: AsRef<str>>(
ressource: S1,
request_id: S2,
kind: impl Into<S3Error>,
) -> Self {
Self {
request_id: request_id.as_ref().to_string(),
ressource: ressource.as_ref().to_string(),
kind: Box::new(kind.into()),
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Error {
pub code: String,
pub message: String,
pub resource: String,
pub request_id: String,
}
impl IntoResponse for S3HTTPError {
fn into_response(self) -> axum::response::Response {
let err = match quick_xml::se::to_string(&Error {
code: self.kind.to_string(),
message: self.kind.message().to_string(),
resource: self.ressource,
request_id: self.request_id,
}) {
Ok(elt) => elt,
Err(err) => {
error!("{err:?}");
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::new("wtf".to_string()))
.unwrap();
}
};
let body = format!(
r###"<?xml version="1.0" encoding="UTF-8"?>
{err}
"###,
err = err
);
Response::builder()
.status(self.kind.status_code())
.header(axum::http::header::CONTENT_TYPE, "application/xml")
.body(Body::new(body))
.unwrap()
}
}
impl From<BucketStorageError> for S3Error {
fn from(value: BucketStorageError) -> Self {
match value {
BucketStorageError::Unknown => {
S3ErrorCodeKind::InternalError.into()
}
BucketStorageError::DatabaseAlreadyExist => {
S3ErrorCodeKind::BucketAlreadyExists.into()
}
BucketStorageError::NoBucket => {
S3ErrorCodeKind::NoSuchBucket.into()
}
BucketStorageError::NoKey => S3ErrorCodeKind::NoSuchKey.into(),
}
}
}
#[cfg(all(test, not(target_arch = "wasm32"), not(target_os = "wasi")))]
mod tests {
use axum::response::IntoResponse;
use http_body_util::BodyExt;
use super::{S3ErrorCodeKind, S3HTTPError};
#[tokio::test]
async fn simple_response_error() {
let response =
S3HTTPError::custom("test", "blbl", S3ErrorCodeKind::InternalError)
.into_response();
insta::assert_debug_snapshot!(response, @r###"
Response {
status: 500,
version: HTTP/1.1,
headers: {
"content-type": "application/xml",
},
body: Body(
UnsyncBoxBody,
),
}
"###);
let body = response.into_body().collect().await.unwrap();
let result = String::from_utf8(body.to_bytes().to_vec()).unwrap();
insta::assert_display_snapshot!(result, @r###"
<?xml version="1.0" encoding="UTF-8"?>
<Error><code>InternalError</code><message>An internal error occurred. Try again.</message><resource>test</resource><request_id>blbl</request_id></Error>
"###);
}
}