7f01cd4
use agent_types::api::ApiErrorCode;
#[derive(Debug, thiserror::Error)]
pub enum SdkError {
#[error("transport: {0}")]
Transport(String),
#[error("unauthorized: {0}")]
Unauthorized(String),
#[error("forbidden: {0}")]
Forbidden(String),
#[error("not found: {0}")]
NotFound(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("gone: {0}")]
Gone(String),
#[error("rate limited: {0}")]
RateLimited(String),
#[error("bad request: {0}")]
BadRequest(String),
#[error("server error ({status}): {message}")]
Server { status: u16, message: String },
#[error("decode: {0}")]
Decode(String),
#[error("invalid argument: {0}")]
Invalid(String),
}
impl SdkError {
#[must_use]
pub const fn is_retryable(&self) -> bool {
match self {
Self::Transport(_) | Self::RateLimited(_) => true,
Self::Server { status, .. } => *status >= 500,
_ => false,
}
}
pub(crate) fn from_response(status: u16, body: &str) -> Self {
let parsed: Option<agent_types::api::ApiError> = serde_json::from_str(body).ok();
let message = parsed
.as_ref()
.map(|e| e.message.to_string())
.unwrap_or_else(|| body.trim().to_string());
match crate::error::code_from_status(status, parsed.as_ref().map(|e| e.code)) {
ApiErrorCode::Unauthenticated => Self::Unauthorized(message),
ApiErrorCode::Forbidden => Self::Forbidden(message),
ApiErrorCode::NotFound => Self::NotFound(message),
ApiErrorCode::Conflict => Self::Conflict(message),
ApiErrorCode::Gone => Self::Gone(message),
ApiErrorCode::TooManyRequests => Self::RateLimited(message),
ApiErrorCode::BadRequest => Self::BadRequest(message),
_ => Self::Server { status, message },
}
}
}
pub(crate) fn code_from_status(status: u16, code: Option<ApiErrorCode>) -> ApiErrorCode {
if let Some(code) = code {
return code;
}
match status {
400 => ApiErrorCode::BadRequest,
401 => ApiErrorCode::Unauthenticated,
403 => ApiErrorCode::Forbidden,
404 => ApiErrorCode::NotFound,
409 => ApiErrorCode::Conflict,
410 => ApiErrorCode::Gone,
429 => ApiErrorCode::TooManyRequests,
_ => ApiErrorCode::Internal,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_body_drives_the_variant() {
let body = r#"{"code":"forbidden","message":"跨会话访问被拒绝"}"#;
let err = SdkError::from_response(403, body);
assert!(matches!(err, SdkError::Forbidden(ref m) if m == "跨会话访问被拒绝"));
}
#[test]
fn non_json_body_falls_back_to_status() {
let err = SdkError::from_response(404, "not found");
assert!(matches!(err, SdkError::NotFound(ref m) if m == "not found"));
}
#[test]
fn retryability_matches_transport_and_5xx_only() {
assert!(SdkError::Server { status: 503, message: "down".into() }.is_retryable());
assert!(SdkError::Transport("timeout".into()).is_retryable());
assert!(!SdkError::BadRequest("bad".into()).is_retryable());
assert!(!SdkError::NotFound("x".into()).is_retryable());
assert!(!SdkError::Forbidden("x".into()).is_retryable());
}
}