1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! Error types for the Cycles client.
use std::time::Duration;
use crate::models::ErrorCode;
/// The error type for all Cycles client operations.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// HTTP transport error (network failure, timeout, DNS, etc.).
#[error("HTTP transport error: {0}")]
Transport(#[source] reqwest::Error),
/// The server returned an error response.
#[error("API error (HTTP {status}): {message}")]
Api {
/// HTTP status code.
status: u16,
/// Parsed error code from the response body.
code: Option<ErrorCode>,
/// Human-readable error message.
message: String,
/// Server-assigned request ID.
request_id: Option<String>,
/// Suggested retry delay.
retry_after: Option<Duration>,
/// Additional error details.
details: Option<serde_json::Value>,
},
/// Budget is insufficient for the requested operation (HTTP 409).
#[error("budget exceeded: {message}")]
BudgetExceeded {
/// Human-readable error message.
message: String,
/// Scopes that are over budget.
affected_scopes: Vec<String>,
/// Suggested retry delay.
retry_after: Option<Duration>,
/// Server-assigned request ID.
request_id: Option<String>,
},
/// Failed to deserialize the response body.
#[error("failed to deserialize response: {0}")]
Deserialization(#[source] serde_json::Error),
/// Invalid client configuration.
#[error("invalid configuration: {0}")]
Config(String),
/// Invalid request parameters (caught before sending).
#[error("invalid request: {0}")]
Validation(String),
}
impl Error {
/// Returns `true` if the error is retryable.
///
/// Transport errors and server errors (5xx) are generally retryable.
/// Budget exceeded errors are not retryable unless the server suggests a retry delay.
pub fn is_retryable(&self) -> bool {
match self {
Self::Transport(_) => true,
Self::Api { status, code, .. } => {
if *status >= 500 {
return true;
}
code.is_some_and(|c| c.is_retryable())
}
Self::BudgetExceeded { retry_after, .. } => retry_after.is_some(),
Self::Deserialization(_) | Self::Config(_) | Self::Validation(_) => false,
}
}
/// Returns `true` if this is a budget exceeded error.
pub fn is_budget_exceeded(&self) -> bool {
matches!(self, Self::BudgetExceeded { .. })
|| matches!(
self,
Self::Api {
code: Some(ErrorCode::BudgetExceeded),
..
}
)
}
/// Returns `true` if this is a tenant-closed error (`TENANT_CLOSED`).
///
/// Servers return HTTP 409 `TENANT_CLOSED` on reservation
/// create/commit/release/extend when the owning tenant's status is
/// CLOSED (runtime spec v0.1.25.13, mirroring governance spec Rule 2).
/// Not retryable — the tenant must be reopened administratively.
pub fn is_tenant_closed(&self) -> bool {
matches!(
self,
Self::Api {
code: Some(ErrorCode::TenantClosed),
..
}
)
}
/// Returns the suggested retry delay, if any.
pub fn retry_after(&self) -> Option<Duration> {
match self {
Self::Api { retry_after, .. } => *retry_after,
Self::BudgetExceeded { retry_after, .. } => *retry_after,
_ => None,
}
}
/// Returns the server-assigned request ID, if available.
pub fn request_id(&self) -> Option<&str> {
match self {
Self::Api { request_id, .. } => request_id.as_deref(),
Self::BudgetExceeded { request_id, .. } => request_id.as_deref(),
_ => None,
}
}
/// Returns the error code, if available.
pub fn error_code(&self) -> Option<ErrorCode> {
match self {
Self::Api { code, .. } => *code,
Self::BudgetExceeded { .. } => Some(ErrorCode::BudgetExceeded),
_ => None,
}
}
}
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
Self::Transport(err)
}
}