Skip to main content

opendev_http/models/
mod.rs

1//! Shared types for HTTP operations.
2
3mod retry;
4
5pub use retry::{RetryConfig, classify_retryable_error, parse_retry_after};
6
7/// Errors that can occur during HTTP operations.
8#[derive(Debug, thiserror::Error)]
9pub enum HttpError {
10    #[error("HTTP request failed: {0}")]
11    Request(#[from] reqwest::Error),
12
13    #[error("Interrupted by user")]
14    Interrupted,
15
16    #[error("All retries exhausted: {message}")]
17    RetriesExhausted { message: String },
18
19    #[error("Authentication error: {0}")]
20    Auth(String),
21
22    #[error("IO error: {0}")]
23    Io(#[from] std::io::Error),
24
25    #[error("JSON error: {0}")]
26    Json(#[from] serde_json::Error),
27
28    #[error("{0}")]
29    Other(String),
30}
31
32/// Result of an HTTP request attempt.
33#[derive(Debug)]
34pub struct HttpResult {
35    /// Whether the request completed successfully.
36    pub success: bool,
37    /// HTTP status code, if a response was received.
38    pub status: Option<u16>,
39    /// Response body, if available.
40    pub body: Option<serde_json::Value>,
41    /// Error message, if the request failed.
42    pub error: Option<String>,
43    /// Whether the request was interrupted by the user.
44    pub interrupted: bool,
45    /// Whether the failure is transient and worth retrying.
46    pub retryable: bool,
47    /// Unique request identifier for end-to-end tracing.
48    /// Propagated via the `X-Request-ID` header.
49    pub request_id: Option<String>,
50    /// Value of the `Retry-After` response header, if present.
51    /// Used to honor server-requested retry delays on 429/503 responses.
52    pub retry_after: Option<String>,
53    /// Value of the `retry-after-ms` response header, if present.
54    /// More precise than `Retry-After` (milliseconds instead of seconds).
55    pub retry_after_ms: Option<String>,
56}
57
58impl HttpResult {
59    /// Create a successful result.
60    pub fn ok(status: u16, body: serde_json::Value) -> Self {
61        Self {
62            success: true,
63            status: Some(status),
64            body: Some(body),
65            error: None,
66            interrupted: false,
67            retryable: false,
68            request_id: None,
69            retry_after: None,
70            retry_after_ms: None,
71        }
72    }
73
74    /// Create a failed result.
75    pub fn fail(error: impl Into<String>, retryable: bool) -> Self {
76        Self {
77            success: false,
78            status: None,
79            body: None,
80            error: Some(error.into()),
81            interrupted: false,
82            retryable,
83            request_id: None,
84            retry_after: None,
85            retry_after_ms: None,
86        }
87    }
88
89    /// Create an interrupted result.
90    pub fn interrupted() -> Self {
91        Self {
92            success: false,
93            status: None,
94            body: None,
95            error: Some("Interrupted by user".into()),
96            interrupted: true,
97            retryable: false,
98            request_id: None,
99            retry_after: None,
100            retry_after_ms: None,
101        }
102    }
103
104    /// Create a result from an HTTP response with a retryable status.
105    pub fn retryable_status(
106        status: u16,
107        body: Option<serde_json::Value>,
108        retry_after: Option<String>,
109    ) -> Self {
110        Self {
111            success: false,
112            status: Some(status),
113            body,
114            error: Some(format!("HTTP {status}")),
115            interrupted: false,
116            retryable: true,
117            request_id: None,
118            retry_after,
119            retry_after_ms: None,
120        }
121    }
122
123    /// Attach a request ID to this result for tracing.
124    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
125        self.request_id = Some(request_id.into());
126        self
127    }
128}
129
130#[cfg(test)]
131mod tests;