use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(i32)]
pub enum BingErrorCode {
None = 0,
InternalError = 1,
UnknownError = 2,
InvalidApiKey = 3,
ThrottleUser = 4,
ThrottleHost = 5,
UserBlocked = 6,
InvalidUrl = 7,
InvalidParameter = 8,
TooManySites = 9,
UserNotFound = 10,
NotFound = 11,
AlreadyExists = 12,
NotAllowed = 13,
NotAuthorized = 14,
UnexpectedState = 15,
Deprecated = 16,
}
impl BingErrorCode {
pub fn from_i32(value: i32) -> Option<Self> {
match value {
0 => Some(Self::None),
1 => Some(Self::InternalError),
2 => Some(Self::UnknownError),
3 => Some(Self::InvalidApiKey),
4 => Some(Self::ThrottleUser),
5 => Some(Self::ThrottleHost),
6 => Some(Self::UserBlocked),
7 => Some(Self::InvalidUrl),
8 => Some(Self::InvalidParameter),
9 => Some(Self::TooManySites),
10 => Some(Self::UserNotFound),
11 => Some(Self::NotFound),
12 => Some(Self::AlreadyExists),
13 => Some(Self::NotAllowed),
14 => Some(Self::NotAuthorized),
15 => Some(Self::UnexpectedState),
16 => Some(Self::Deprecated),
_ => None,
}
}
pub fn to_i32(self) -> i32 {
self as i32
}
}
impl fmt::Display for BingErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::None => "None",
Self::InternalError => "InternalError",
Self::UnknownError => "UnknownError",
Self::InvalidApiKey => "InvalidApiKey",
Self::ThrottleUser => "ThrottleUser",
Self::ThrottleHost => "ThrottleHost",
Self::UserBlocked => "UserBlocked",
Self::InvalidUrl => "InvalidUrl",
Self::InvalidParameter => "InvalidParameter",
Self::TooManySites => "TooManySites",
Self::UserNotFound => "UserNotFound",
Self::NotFound => "NotFound",
Self::AlreadyExists => "AlreadyExists",
Self::NotAllowed => "NotAllowed",
Self::NotAuthorized => "NotAuthorized",
Self::UnexpectedState => "UnexpectedState",
Self::Deprecated => "Deprecated",
};
write!(f, "{}", name)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum ErrorCodeWrapper {
Structured { value: i32 },
Raw(i32),
}
impl ErrorCodeWrapper {
fn value(&self) -> i32 {
match self {
Self::Structured { value } => *value,
Self::Raw(value) => *value,
}
}
}
#[derive(Debug, Clone, Deserialize)]
struct ApiErrorResponse {
#[serde(rename = "ErrorCode")]
error_code: ErrorCodeWrapper,
#[serde(rename = "Message")]
message: String,
}
#[derive(Debug, Error)]
pub enum WebmasterApiError {
#[error("HTTP request failed: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Middleware request failed: {0}")]
MiddlewareHttpError(#[from] reqwest_middleware::Error),
#[error("Failed to parse response: {0}")]
ParseError(#[from] serde_json::Error),
#[error("API error ({error_code_raw}): {message}")]
ApiError {
status: Option<u16>,
error_code: Option<BingErrorCode>,
error_code_raw: i32,
message: String,
},
#[error("HTTP {status}: {message}")]
HttpStatusError {
status: u16,
message: String,
response_body: Option<String>,
},
#[error("Invalid API response: {0}")]
InvalidResponse(String),
#[error("Authentication failed: missing or invalid API key")]
AuthenticationError,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl WebmasterApiError {
pub fn api_error(error_code_raw: i32, message: String, status: Option<u16>) -> Self {
let error_code = BingErrorCode::from_i32(error_code_raw);
Self::ApiError {
status,
error_code,
error_code_raw,
message,
}
}
pub fn http_status(status: u16, message: impl Into<String>) -> Self {
Self::HttpStatusError {
status,
message: message.into(),
response_body: None,
}
}
pub fn http_status_with_body(
status: u16,
message: impl Into<String>,
response_body: impl Into<String>,
) -> Self {
Self::HttpStatusError {
status,
message: message.into(),
response_body: Some(response_body.into()),
}
}
pub fn invalid_response(message: impl Into<String>) -> Self {
Self::InvalidResponse(message.into())
}
pub fn is_retryable(&self) -> bool {
match self {
Self::HttpError(_) | Self::MiddlewareHttpError(_) => true,
Self::HttpStatusError { status, .. } => {
matches!(status, 429 | 500 | 502 | 503 | 504)
}
Self::ApiError { error_code, .. } => {
matches!(
error_code,
Some(BingErrorCode::ThrottleUser) | Some(BingErrorCode::ThrottleHost)
)
}
_ => false,
}
}
pub fn is_authentication_error(&self) -> bool {
match self {
Self::AuthenticationError => true,
Self::HttpStatusError { status, .. } => *status == 401 || *status == 403,
Self::ApiError { error_code, .. } => {
matches!(
error_code,
Some(BingErrorCode::InvalidApiKey) | Some(BingErrorCode::NotAuthorized)
)
}
_ => false,
}
}
pub fn is_rate_limit_error(&self) -> bool {
match self {
Self::HttpStatusError { status, .. } => *status == 429,
Self::ApiError { error_code, .. } => {
matches!(
error_code,
Some(BingErrorCode::ThrottleUser) | Some(BingErrorCode::ThrottleHost)
)
}
_ => false,
}
}
pub fn status_code(&self) -> Option<u16> {
match self {
Self::HttpStatusError { status, .. } => Some(*status),
Self::ApiError { status, .. } => *status,
Self::HttpError(err) => err.status().map(|s| s.as_u16()),
_ => None,
}
}
pub fn response_body(&self) -> Option<&str> {
match self {
Self::HttpStatusError { response_body, .. } => response_body.as_deref(),
_ => None,
}
}
pub fn error_code(&self) -> Option<std::result::Result<BingErrorCode, i32>> {
match self {
Self::ApiError {
error_code,
error_code_raw,
..
} => Some(error_code.ok_or(*error_code_raw)),
_ => None,
}
}
pub fn api_message(&self) -> Option<&str> {
match self {
Self::ApiError { message, .. } => Some(message.as_str()),
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, WebmasterApiError>;
pub fn map_status_error(status: reqwest::StatusCode, response_text: String) -> WebmasterApiError {
let status_code = status.as_u16();
let message = match status_code {
400 => "Bad Request - The request is invalid or malformed",
401 => "Unauthorized - Invalid or missing API key",
403 => "Forbidden - Access denied to the requested resource",
404 => "Not Found - The requested resource was not found",
429 => "Too Many Requests - Rate limit exceeded",
500 => "Internal Server Error - Server encountered an error",
502 => "Bad Gateway - Invalid response from upstream server",
503 => "Service Unavailable - Service temporarily unavailable",
504 => "Gateway Timeout - Request timeout",
_ => "HTTP request failed",
};
match status_code {
401 | 403 => WebmasterApiError::AuthenticationError,
404 => WebmasterApiError::InvalidResponse(format!("{}: {}", message, response_text)),
_ => WebmasterApiError::http_status_with_body(status_code, message, response_text),
}
}
pub fn try_parse_api_error(response_text: &str) -> Option<(i32, String)> {
if let Ok(wrapper) =
serde_json::from_str::<crate::dto::ResponseWrapper<ApiErrorResponse>>(response_text)
{
return Some((wrapper.d.error_code.value(), wrapper.d.message));
}
serde_json::from_str::<ApiErrorResponse>(response_text)
.ok()
.map(|r| (r.error_code.value(), r.message))
}