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