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 TlsCertExpired,
36 TlsHostnameMismatch,
38 TlsUntrustedRoot,
40 TlsError,
42 Busy,
44}
45
46impl ErrorCode {
47 pub fn as_str(self) -> &'static str {
48 match self {
49 ErrorCode::RobotsDisallowed => "ROBOTS_DISALLOWED",
50 ErrorCode::AccessBarrier => "ACCESS_BARRIER",
51 ErrorCode::RateLimited => "RATE_LIMITED",
52 ErrorCode::HostCooldown => "HOST_COOLDOWN",
53 ErrorCode::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
54 ErrorCode::ConcurrencyLimitExceeded => "CONCURRENCY_LIMIT_EXCEEDED",
55 ErrorCode::QuotaExceeded => "QUOTA_EXCEEDED",
56 ErrorCode::Timeout => "TIMEOUT",
57 ErrorCode::SsrfBlocked => "SSRF_BLOCKED",
58 ErrorCode::TooLarge => "TOO_LARGE",
59 ErrorCode::UpstreamError => "UPSTREAM_ERROR",
60 ErrorCode::UnsupportedOption => "UNSUPPORTED_OPTION",
61 ErrorCode::InvalidRequest => "INVALID_REQUEST",
62 ErrorCode::Unauthorized => "UNAUTHORIZED",
63 ErrorCode::NotFound => "NOT_FOUND",
64 ErrorCode::Internal => "INTERNAL_ERROR",
65 ErrorCode::DnsFailure => "DNS_FAILURE",
66 ErrorCode::NetworkError => "NETWORK_ERROR",
67 ErrorCode::TlsCertExpired => "TLS_CERT_EXPIRED",
68 ErrorCode::TlsHostnameMismatch => "TLS_HOSTNAME_MISMATCH",
69 ErrorCode::TlsUntrustedRoot => "TLS_UNTRUSTED_ROOT",
70 ErrorCode::TlsError => "TLS_ERROR",
71 ErrorCode::Busy => "BUSY",
72 }
73 }
74
75 pub fn status(self) -> StatusCode {
76 match self {
77 ErrorCode::RobotsDisallowed | ErrorCode::AccessBarrier => StatusCode::FORBIDDEN,
78 ErrorCode::RateLimited
79 | ErrorCode::RateLimitExceeded
80 | ErrorCode::ConcurrencyLimitExceeded
81 | ErrorCode::QuotaExceeded => StatusCode::TOO_MANY_REQUESTS,
82 ErrorCode::HostCooldown | ErrorCode::Busy => StatusCode::SERVICE_UNAVAILABLE,
83 ErrorCode::Timeout => StatusCode::REQUEST_TIMEOUT,
84 ErrorCode::SsrfBlocked | ErrorCode::UnsupportedOption | ErrorCode::InvalidRequest => {
85 StatusCode::BAD_REQUEST
86 }
87 ErrorCode::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
88 ErrorCode::UpstreamError | ErrorCode::DnsFailure | ErrorCode::NetworkError => {
89 StatusCode::BAD_GATEWAY
90 }
91 ErrorCode::TlsCertExpired
92 | ErrorCode::TlsHostnameMismatch
93 | ErrorCode::TlsUntrustedRoot
94 | ErrorCode::TlsError => StatusCode::BAD_GATEWAY,
95 ErrorCode::Unauthorized => StatusCode::UNAUTHORIZED,
96 ErrorCode::NotFound => StatusCode::NOT_FOUND,
97 ErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq)]
103pub struct ApiError {
104 pub code: ErrorCode,
105 pub message: String,
106 pub details: Option<Value>,
108 pub retry_after: Option<u64>,
110}
111
112impl ApiError {
113 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
114 Self {
115 code,
116 message: message.into(),
117 details: None,
118 retry_after: None,
119 }
120 }
121
122 pub fn with_details(mut self, details: Value) -> Self {
123 self.details = Some(details);
124 self
125 }
126
127 pub fn with_retry_after(mut self, secs: u64) -> Self {
128 self.retry_after = Some(secs);
129 self
130 }
131
132 pub fn unsupported(what: impl Into<String>) -> Self {
133 Self::new(ErrorCode::UnsupportedOption, what)
134 }
135
136 pub fn invalid(what: impl Into<String>) -> Self {
137 Self::new(ErrorCode::InvalidRequest, what)
138 }
139}
140
141impl std::fmt::Display for ApiError {
142 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(f, "{}: {}", self.code.as_str(), self.message)
144 }
145}
146
147impl std::error::Error for ApiError {}
148
149impl IntoResponse for ApiError {
150 fn into_response(self) -> Response {
151 let mut body = json!({
152 "success": false,
153 "code": self.code.as_str(),
154 "error": self.message,
155 });
156 if let Some(d) = self.details {
157 body["details"] = d;
158 }
159 let mut resp = (self.code.status(), Json(body)).into_response();
160 resp.extensions_mut().insert(self.code);
162 if let Some(secs) = self.retry_after
163 && let Ok(v) = secs.to_string().parse()
164 {
165 resp.headers_mut().insert("retry-after", v);
166 }
167 resp
168 }
169}