use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
pub type ProxyResult<T> = Result<T, ProxyError>;
#[derive(Debug, thiserror::Error)]
pub enum ProxyError {
#[error("not found: {0}")]
NotFound(String),
#[error("unauthenticated: {0}")]
Unauthenticated(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("out of scope: {0}")]
OutOfScope(String),
#[error("invalid request: {0}")]
InvalidArgument(String),
#[error("precondition failed: {0}")]
PreconditionFailed(String),
#[error("range not satisfiable: {0}")]
RangeNotSatisfiable(String),
#[error("payload too large: {0}")]
PayloadTooLarge(String),
#[error("not implemented: {0}")]
NotImplemented(&'static str),
#[error(transparent)]
Storage(#[from] object_store::Error),
#[error("internal error: {0}")]
Internal(String),
}
impl ProxyError {
fn storage_parts(err: &object_store::Error) -> StatusCode {
match err {
object_store::Error::NotFound { .. } => StatusCode::NOT_FOUND,
object_store::Error::Precondition { .. } => StatusCode::PRECONDITION_FAILED,
object_store::Error::NotModified { .. } => StatusCode::NOT_MODIFIED,
object_store::Error::AlreadyExists { .. } => StatusCode::CONFLICT,
object_store::Error::PermissionDenied { .. } => StatusCode::FORBIDDEN,
object_store::Error::Unauthenticated { .. } => StatusCode::UNAUTHORIZED,
object_store::Error::NotSupported { .. }
| object_store::Error::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED,
object_store::Error::InvalidPath { .. } => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
fn status(&self) -> StatusCode {
match self {
ProxyError::NotFound(_) => StatusCode::NOT_FOUND,
ProxyError::Unauthenticated(_) => StatusCode::UNAUTHORIZED,
ProxyError::PermissionDenied(_) | ProxyError::OutOfScope(_) => StatusCode::FORBIDDEN,
ProxyError::InvalidArgument(_) => StatusCode::BAD_REQUEST,
ProxyError::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
ProxyError::RangeNotSatisfiable(_) => StatusCode::RANGE_NOT_SATISFIABLE,
ProxyError::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
ProxyError::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
ProxyError::Storage(e) => Self::storage_parts(e),
ProxyError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl IntoResponse for ProxyError {
fn into_response(self) -> Response {
let status = self.status();
let message = self.to_string();
let body = format!("{{\"error\":{}}}", serde_json_string(&message));
(status, [(header::CONTENT_TYPE, "application/json")], body).into_response()
}
}
fn serde_json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn statuses_match_contract() {
assert_eq!(
ProxyError::OutOfScope("x".into()).status(),
StatusCode::FORBIDDEN
);
assert_eq!(
ProxyError::PreconditionFailed("x".into()).status(),
StatusCode::PRECONDITION_FAILED
);
assert_eq!(
ProxyError::RangeNotSatisfiable("x".into()).status(),
StatusCode::RANGE_NOT_SATISFIABLE
);
assert_eq!(
ProxyError::NotImplemented("gcp").status(),
StatusCode::NOT_IMPLEMENTED
);
}
#[test]
fn object_store_precondition_maps_to_412() {
let err = object_store::Error::Precondition {
path: "p".into(),
source: "etag mismatch".into(),
};
assert_eq!(
ProxyError::Storage(err).status(),
StatusCode::PRECONDITION_FAILED
);
}
#[test]
fn object_store_not_found_maps_to_404() {
let err = object_store::Error::NotFound {
path: "p".into(),
source: "missing".into(),
};
assert_eq!(ProxyError::Storage(err).status(), StatusCode::NOT_FOUND);
}
#[test]
fn json_escaping() {
assert_eq!(serde_json_string("a\"b\\c"), r#""a\"b\\c""#);
assert_eq!(serde_json_string("line\nbreak"), r#""line\nbreak""#);
}
}