1use axum::Json;
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde_json::{Value, json};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ErrorCode {
11 RobotsDisallowed,
12 AccessBarrier,
13 RateLimited,
14 HostCooldown,
15 RateLimitExceeded,
17 ConcurrencyLimitExceeded,
19 QuotaExceeded,
21 Timeout,
22 SsrfBlocked,
23 TooLarge,
24 UpstreamError,
25 UnsupportedOption,
26 InvalidRequest,
27 Unauthorized,
28 NotFound,
29 Internal,
30 DnsFailure,
32 NetworkError,
34 Busy,
36}
37
38impl ErrorCode {
39 pub fn as_str(self) -> &'static str {
40 match self {
41 ErrorCode::RobotsDisallowed => "ROBOTS_DISALLOWED",
42 ErrorCode::AccessBarrier => "ACCESS_BARRIER",
43 ErrorCode::RateLimited => "RATE_LIMITED",
44 ErrorCode::HostCooldown => "HOST_COOLDOWN",
45 ErrorCode::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
46 ErrorCode::ConcurrencyLimitExceeded => "CONCURRENCY_LIMIT_EXCEEDED",
47 ErrorCode::QuotaExceeded => "QUOTA_EXCEEDED",
48 ErrorCode::Timeout => "TIMEOUT",
49 ErrorCode::SsrfBlocked => "SSRF_BLOCKED",
50 ErrorCode::TooLarge => "TOO_LARGE",
51 ErrorCode::UpstreamError => "UPSTREAM_ERROR",
52 ErrorCode::UnsupportedOption => "UNSUPPORTED_OPTION",
53 ErrorCode::InvalidRequest => "INVALID_REQUEST",
54 ErrorCode::Unauthorized => "UNAUTHORIZED",
55 ErrorCode::NotFound => "NOT_FOUND",
56 ErrorCode::Internal => "INTERNAL_ERROR",
57 ErrorCode::DnsFailure => "DNS_FAILURE",
58 ErrorCode::NetworkError => "NETWORK_ERROR",
59 ErrorCode::Busy => "BUSY",
60 }
61 }
62
63 pub fn status(self) -> StatusCode {
64 match self {
65 ErrorCode::RobotsDisallowed | ErrorCode::AccessBarrier => StatusCode::FORBIDDEN,
66 ErrorCode::RateLimited
67 | ErrorCode::RateLimitExceeded
68 | ErrorCode::ConcurrencyLimitExceeded
69 | ErrorCode::QuotaExceeded => StatusCode::TOO_MANY_REQUESTS,
70 ErrorCode::HostCooldown | ErrorCode::Busy => StatusCode::SERVICE_UNAVAILABLE,
71 ErrorCode::Timeout => StatusCode::REQUEST_TIMEOUT,
72 ErrorCode::SsrfBlocked | ErrorCode::UnsupportedOption | ErrorCode::InvalidRequest => {
73 StatusCode::BAD_REQUEST
74 }
75 ErrorCode::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
76 ErrorCode::UpstreamError | ErrorCode::DnsFailure | ErrorCode::NetworkError => {
77 StatusCode::BAD_GATEWAY
78 }
79 ErrorCode::Unauthorized => StatusCode::UNAUTHORIZED,
80 ErrorCode::NotFound => StatusCode::NOT_FOUND,
81 ErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct ApiError {
88 pub code: ErrorCode,
89 pub message: String,
90 pub details: Option<Value>,
92 pub retry_after: Option<u64>,
94}
95
96impl ApiError {
97 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
98 Self {
99 code,
100 message: message.into(),
101 details: None,
102 retry_after: None,
103 }
104 }
105
106 pub fn with_details(mut self, details: Value) -> Self {
107 self.details = Some(details);
108 self
109 }
110
111 pub fn with_retry_after(mut self, secs: u64) -> Self {
112 self.retry_after = Some(secs);
113 self
114 }
115
116 pub fn unsupported(what: impl Into<String>) -> Self {
117 Self::new(ErrorCode::UnsupportedOption, what)
118 }
119
120 pub fn invalid(what: impl Into<String>) -> Self {
121 Self::new(ErrorCode::InvalidRequest, what)
122 }
123}
124
125impl std::fmt::Display for ApiError {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(f, "{}: {}", self.code.as_str(), self.message)
128 }
129}
130
131impl std::error::Error for ApiError {}
132
133impl IntoResponse for ApiError {
134 fn into_response(self) -> Response {
135 let mut body = json!({
136 "success": false,
137 "code": self.code.as_str(),
138 "error": self.message,
139 });
140 if let Some(d) = self.details {
141 body["details"] = d;
142 }
143 let mut resp = (self.code.status(), Json(body)).into_response();
144 resp.extensions_mut().insert(self.code);
146 if let Some(secs) = self.retry_after
147 && let Ok(v) = secs.to_string().parse()
148 {
149 resp.headers_mut().insert("retry-after", v);
150 }
151 resp
152 }
153}