use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::{Value, json};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
RobotsDisallowed,
AccessBarrier,
RateLimited,
HostCooldown,
RateLimitExceeded,
ConcurrencyLimitExceeded,
QuotaExceeded,
Timeout,
SsrfBlocked,
TooLarge,
UpstreamError,
UnsupportedOption,
InvalidRequest,
Unauthorized,
NotFound,
Internal,
DnsFailure,
NetworkError,
Busy,
}
impl ErrorCode {
pub fn as_str(self) -> &'static str {
match self {
ErrorCode::RobotsDisallowed => "ROBOTS_DISALLOWED",
ErrorCode::AccessBarrier => "ACCESS_BARRIER",
ErrorCode::RateLimited => "RATE_LIMITED",
ErrorCode::HostCooldown => "HOST_COOLDOWN",
ErrorCode::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
ErrorCode::ConcurrencyLimitExceeded => "CONCURRENCY_LIMIT_EXCEEDED",
ErrorCode::QuotaExceeded => "QUOTA_EXCEEDED",
ErrorCode::Timeout => "TIMEOUT",
ErrorCode::SsrfBlocked => "SSRF_BLOCKED",
ErrorCode::TooLarge => "TOO_LARGE",
ErrorCode::UpstreamError => "UPSTREAM_ERROR",
ErrorCode::UnsupportedOption => "UNSUPPORTED_OPTION",
ErrorCode::InvalidRequest => "INVALID_REQUEST",
ErrorCode::Unauthorized => "UNAUTHORIZED",
ErrorCode::NotFound => "NOT_FOUND",
ErrorCode::Internal => "INTERNAL_ERROR",
ErrorCode::DnsFailure => "DNS_FAILURE",
ErrorCode::NetworkError => "NETWORK_ERROR",
ErrorCode::Busy => "BUSY",
}
}
pub fn status(self) -> StatusCode {
match self {
ErrorCode::RobotsDisallowed | ErrorCode::AccessBarrier => StatusCode::FORBIDDEN,
ErrorCode::RateLimited
| ErrorCode::RateLimitExceeded
| ErrorCode::ConcurrencyLimitExceeded
| ErrorCode::QuotaExceeded => StatusCode::TOO_MANY_REQUESTS,
ErrorCode::HostCooldown | ErrorCode::Busy => StatusCode::SERVICE_UNAVAILABLE,
ErrorCode::Timeout => StatusCode::REQUEST_TIMEOUT,
ErrorCode::SsrfBlocked | ErrorCode::UnsupportedOption | ErrorCode::InvalidRequest => {
StatusCode::BAD_REQUEST
}
ErrorCode::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
ErrorCode::UpstreamError | ErrorCode::DnsFailure | ErrorCode::NetworkError => {
StatusCode::BAD_GATEWAY
}
ErrorCode::Unauthorized => StatusCode::UNAUTHORIZED,
ErrorCode::NotFound => StatusCode::NOT_FOUND,
ErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ApiError {
pub code: ErrorCode,
pub message: String,
pub details: Option<Value>,
pub retry_after: Option<u64>,
}
impl ApiError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
details: None,
retry_after: None,
}
}
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
}
pub fn with_retry_after(mut self, secs: u64) -> Self {
self.retry_after = Some(secs);
self
}
pub fn unsupported(what: impl Into<String>) -> Self {
Self::new(ErrorCode::UnsupportedOption, what)
}
pub fn invalid(what: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidRequest, what)
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code.as_str(), self.message)
}
}
impl std::error::Error for ApiError {}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let mut body = json!({
"success": false,
"code": self.code.as_str(),
"error": self.message,
});
if let Some(d) = self.details {
body["details"] = d;
}
let mut resp = (self.code.status(), Json(body)).into_response();
resp.extensions_mut().insert(self.code);
if let Some(secs) = self.retry_after
&& let Ok(v) = secs.to_string().parse()
{
resp.headers_mut().insert("retry-after", v);
}
resp
}
}