use serde::{Deserialize, Serialize};
use thiserror::Error;
pub type Result<T> = std::result::Result<T, ClientError>;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ServerErrorCode {
NamespaceNotFound,
VectorNotFound,
DimensionMismatch,
EmptyVector,
InvalidRequest,
StorageError,
InternalError,
QuotaExceeded,
ServiceUnavailable,
AuthenticationRequired,
InvalidApiKey,
ApiKeyExpired,
InsufficientScope,
NamespaceAccessDenied,
#[serde(other)]
Unknown,
}
#[derive(Error, Debug)]
pub enum ClientError {
#[cfg(feature = "http-client")]
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[cfg(feature = "grpc")]
#[error("gRPC request failed: {0}")]
Grpc(String),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Server error ({status}): {message}")]
Server {
status: u16,
message: String,
#[doc = "Typed error code from the server"]
code: Option<ServerErrorCode>,
},
#[error("Authorization failed ({status}): {message}")]
Authorization {
status: u16,
message: String,
code: Option<ServerErrorCode>,
},
#[error("Invalid configuration: {0}")]
Config(String),
#[error("Namespace not found: {0}")]
NamespaceNotFound(String),
#[error("Vector not found: {0}")]
VectorNotFound(String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Connection failed: {0}")]
Connection(String),
#[error("Request timeout")]
Timeout,
#[error("Rate limit exceeded — retry after {retry_after:?}")]
RateLimitExceeded {
retry_after: Option<u64>,
},
}
impl ClientError {
pub fn is_retryable(&self) -> bool {
match self {
#[cfg(feature = "http-client")]
ClientError::Http(e) => e.is_timeout() || e.is_connect(),
#[cfg(feature = "grpc")]
ClientError::Grpc(_) => true, ClientError::Server { status, .. } => *status >= 500,
ClientError::Connection(_) => true,
ClientError::Timeout => true,
ClientError::RateLimitExceeded { .. } => true,
_ => false,
}
}
pub fn is_not_found(&self) -> bool {
match self {
ClientError::Server { status, code, .. } => {
*status == 404
|| matches!(
code,
Some(ServerErrorCode::NamespaceNotFound)
| Some(ServerErrorCode::VectorNotFound)
)
}
ClientError::NamespaceNotFound(_) => true,
ClientError::VectorNotFound(_) => true,
_ => false,
}
}
pub fn is_auth_error(&self) -> bool {
matches!(self, ClientError::Authorization { .. })
|| matches!(self, ClientError::Server { status: 401, .. })
}
}