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 = 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 { 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()); } }