1use serde::{Deserialize, Serialize};
2use std::fmt;
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[repr(i32)]
13pub enum BingErrorCode {
14 None = 0,
16 InternalError = 1,
18 UnknownError = 2,
20 InvalidApiKey = 3,
22 ThrottleUser = 4,
24 ThrottleHost = 5,
26 UserBlocked = 6,
28 InvalidUrl = 7,
30 InvalidParameter = 8,
32 TooManySites = 9,
34 UserNotFound = 10,
36 NotFound = 11,
38 AlreadyExists = 12,
40 NotAllowed = 13,
42 NotAuthorized = 14,
44 UnexpectedState = 15,
46 Deprecated = 16,
48}
49
50impl BingErrorCode {
51 pub fn from_i32(value: i32) -> Option<Self> {
53 match value {
54 0 => Some(Self::None),
55 1 => Some(Self::InternalError),
56 2 => Some(Self::UnknownError),
57 3 => Some(Self::InvalidApiKey),
58 4 => Some(Self::ThrottleUser),
59 5 => Some(Self::ThrottleHost),
60 6 => Some(Self::UserBlocked),
61 7 => Some(Self::InvalidUrl),
62 8 => Some(Self::InvalidParameter),
63 9 => Some(Self::TooManySites),
64 10 => Some(Self::UserNotFound),
65 11 => Some(Self::NotFound),
66 12 => Some(Self::AlreadyExists),
67 13 => Some(Self::NotAllowed),
68 14 => Some(Self::NotAuthorized),
69 15 => Some(Self::UnexpectedState),
70 16 => Some(Self::Deprecated),
71 _ => None,
72 }
73 }
74
75 pub fn to_i32(self) -> i32 {
77 self as i32
78 }
79}
80
81impl fmt::Display for BingErrorCode {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 let name = match self {
84 Self::None => "None",
85 Self::InternalError => "InternalError",
86 Self::UnknownError => "UnknownError",
87 Self::InvalidApiKey => "InvalidApiKey",
88 Self::ThrottleUser => "ThrottleUser",
89 Self::ThrottleHost => "ThrottleHost",
90 Self::UserBlocked => "UserBlocked",
91 Self::InvalidUrl => "InvalidUrl",
92 Self::InvalidParameter => "InvalidParameter",
93 Self::TooManySites => "TooManySites",
94 Self::UserNotFound => "UserNotFound",
95 Self::NotFound => "NotFound",
96 Self::AlreadyExists => "AlreadyExists",
97 Self::NotAllowed => "NotAllowed",
98 Self::NotAuthorized => "NotAuthorized",
99 Self::UnexpectedState => "UnexpectedState",
100 Self::Deprecated => "Deprecated",
101 };
102 write!(f, "{}", name)
103 }
104}
105
106#[derive(Debug, Clone, Deserialize)]
108#[serde(untagged)]
109enum ErrorCodeWrapper {
110 Structured { value: i32 },
112 Raw(i32),
114}
115
116impl ErrorCodeWrapper {
117 fn value(&self) -> i32 {
118 match self {
119 Self::Structured { value } => *value,
120 Self::Raw(value) => *value,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Deserialize)]
129struct ApiErrorResponse {
130 #[serde(rename = "ErrorCode")]
131 error_code: ErrorCodeWrapper,
132 #[serde(rename = "Message")]
133 message: String,
134}
135
136#[derive(Debug, Error)]
138pub enum WebmasterApiError {
139 #[error("HTTP request failed: {0}")]
141 HttpError(#[from] reqwest::Error),
142
143 #[error("Middleware request failed: {0}")]
145 MiddlewareHttpError(#[from] reqwest_middleware::Error),
146
147 #[error("Failed to parse response: {0}")]
149 ParseError(#[from] serde_json::Error),
150
151 #[error("API error ({error_code_raw}): {message}")]
153 ApiError {
154 status: Option<u16>,
156 error_code: Option<BingErrorCode>,
158 error_code_raw: i32,
160 message: String,
162 },
163
164 #[error("HTTP {status}: {message}")]
166 HttpStatusError {
167 status: u16,
169 message: String,
171 response_body: Option<String>,
173 },
174
175 #[error("Invalid API response: {0}")]
177 InvalidResponse(String),
178
179 #[error("Authentication failed: missing or invalid API key")]
181 AuthenticationError,
182
183 #[error(transparent)]
185 Other(#[from] anyhow::Error),
186}
187
188impl WebmasterApiError {
189 pub fn api_error(error_code_raw: i32, message: String, status: Option<u16>) -> Self {
191 let error_code = BingErrorCode::from_i32(error_code_raw);
192 Self::ApiError {
193 status,
194 error_code,
195 error_code_raw,
196 message,
197 }
198 }
199
200 pub fn http_status(status: u16, message: impl Into<String>) -> Self {
202 Self::HttpStatusError {
203 status,
204 message: message.into(),
205 response_body: None,
206 }
207 }
208
209 pub fn http_status_with_body(
211 status: u16,
212 message: impl Into<String>,
213 response_body: impl Into<String>,
214 ) -> Self {
215 Self::HttpStatusError {
216 status,
217 message: message.into(),
218 response_body: Some(response_body.into()),
219 }
220 }
221
222 pub fn invalid_response(message: impl Into<String>) -> Self {
224 Self::InvalidResponse(message.into())
225 }
226
227 pub fn is_retryable(&self) -> bool {
229 match self {
230 Self::HttpError(_) | Self::MiddlewareHttpError(_) => true,
232 Self::HttpStatusError { status, .. } => {
234 matches!(status, 429 | 500 | 502 | 503 | 504)
235 }
236 Self::ApiError { error_code, .. } => {
238 matches!(
239 error_code,
240 Some(BingErrorCode::ThrottleUser) | Some(BingErrorCode::ThrottleHost)
241 )
242 }
243 _ => false,
244 }
245 }
246
247 pub fn is_authentication_error(&self) -> bool {
249 match self {
250 Self::AuthenticationError => true,
251 Self::HttpStatusError { status, .. } => *status == 401 || *status == 403,
252 Self::ApiError { error_code, .. } => {
253 matches!(
254 error_code,
255 Some(BingErrorCode::InvalidApiKey) | Some(BingErrorCode::NotAuthorized)
256 )
257 }
258 _ => false,
259 }
260 }
261
262 pub fn is_rate_limit_error(&self) -> bool {
264 match self {
265 Self::HttpStatusError { status, .. } => *status == 429,
266 Self::ApiError { error_code, .. } => {
267 matches!(
268 error_code,
269 Some(BingErrorCode::ThrottleUser) | Some(BingErrorCode::ThrottleHost)
270 )
271 }
272 _ => false,
273 }
274 }
275
276 pub fn status_code(&self) -> Option<u16> {
278 match self {
279 Self::HttpStatusError { status, .. } => Some(*status),
280 Self::ApiError { status, .. } => *status,
281 Self::HttpError(err) => err.status().map(|s| s.as_u16()),
282 _ => None,
283 }
284 }
285
286 pub fn response_body(&self) -> Option<&str> {
288 match self {
289 Self::HttpStatusError { response_body, .. } => response_body.as_deref(),
290 _ => None,
291 }
292 }
293
294 pub fn error_code(&self) -> Option<std::result::Result<BingErrorCode, i32>> {
298 match self {
299 Self::ApiError {
300 error_code,
301 error_code_raw,
302 ..
303 } => Some(error_code.ok_or(*error_code_raw)),
304 _ => None,
305 }
306 }
307
308 pub fn api_message(&self) -> Option<&str> {
310 match self {
311 Self::ApiError { message, .. } => Some(message.as_str()),
312 _ => None,
313 }
314 }
315}
316
317pub type Result<T> = std::result::Result<T, WebmasterApiError>;
319
320pub fn map_status_error(status: reqwest::StatusCode, response_text: String) -> WebmasterApiError {
322 let status_code = status.as_u16();
323 let message = match status_code {
324 400 => "Bad Request - The request is invalid or malformed",
325 401 => "Unauthorized - Invalid or missing API key",
326 403 => "Forbidden - Access denied to the requested resource",
327 404 => "Not Found - The requested resource was not found",
328 429 => "Too Many Requests - Rate limit exceeded",
329 500 => "Internal Server Error - Server encountered an error",
330 502 => "Bad Gateway - Invalid response from upstream server",
331 503 => "Service Unavailable - Service temporarily unavailable",
332 504 => "Gateway Timeout - Request timeout",
333 _ => "HTTP request failed",
334 };
335
336 match status_code {
337 401 | 403 => WebmasterApiError::AuthenticationError,
338 404 => WebmasterApiError::InvalidResponse(format!("{}: {}", message, response_text)),
339 _ => WebmasterApiError::http_status_with_body(status_code, message, response_text),
340 }
341}
342
343pub fn try_parse_api_error(response_text: &str) -> Option<(i32, String)> {
347 if let Ok(wrapper) =
349 serde_json::from_str::<crate::dto::ResponseWrapper<ApiErrorResponse>>(response_text)
350 {
351 return Some((wrapper.d.error_code.value(), wrapper.d.message));
352 }
353
354 serde_json::from_str::<ApiErrorResponse>(response_text)
356 .ok()
357 .map(|r| (r.error_code.value(), r.message))
358}