mirror of
https://github.com/matter-labs/teepot.git
synced 2025-08-19 11:13:54 +02:00

- Add `max_retries` field to ApiClient with default of 3 retries - Implement `execute_with_retry()` helper method in helpers.rs - Update all HTTP requests to use retry wrapper for automatic 429 handling - Add `TooManyRequests` error variant with request_id and retry_after fields - Respect Retry-After header duration before retrying requests - Add `set_max_retries()` method to configure retry behavior (0 disables) - Update documentation and add handle_rate_limit example - Enhanced error handling in check_status() for 429 responses The client now transparently handles Intel API rate limiting while remaining configurable for users who need different retry behavior or manual handling. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
159 lines
5.5 KiB
Rust
159 lines
5.5 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2025 Matter Labs
|
|
|
|
use reqwest::{Response, StatusCode};
|
|
use thiserror::Error;
|
|
|
|
/// Represents all possible errors that can occur when interacting with Intel's DCAP API.
|
|
#[derive(Error, Debug)]
|
|
pub enum IntelApiError {
|
|
/// Indicates that the requested API version or feature is unsupported.
|
|
#[error("Unsupported API version or feature: {0}")]
|
|
UnsupportedApiVersion(String),
|
|
|
|
/// Wraps an underlying reqwest error.
|
|
#[error("Reqwest error: {0}")]
|
|
Reqwest(#[from] reqwest::Error),
|
|
|
|
/// Wraps a URL parsing error.
|
|
#[error("URL parsing error: {0}")]
|
|
UrlParse(#[from] url::ParseError),
|
|
|
|
/// Wraps a Serde JSON error.
|
|
#[error("Serde JSON error: {0}")]
|
|
JsonError(#[from] serde_json::Error),
|
|
|
|
/// Represents a general API error, capturing the HTTP status and optional error details.
|
|
#[error("API Error: Status={status}, Request-ID={request_id}, Code={error_code:?}, Message={error_message:?}")]
|
|
ApiError {
|
|
/// HTTP status code returned by the API.
|
|
status: StatusCode,
|
|
/// The unique request identifier for tracing errors.
|
|
request_id: String,
|
|
/// An optional server-provided error code.
|
|
error_code: Option<String>,
|
|
/// An optional server-provided error message.
|
|
error_message: Option<String>,
|
|
},
|
|
|
|
/// Indicates that a header is missing or invalid.
|
|
#[error("Header missing or invalid: {0}")]
|
|
MissingOrInvalidHeader(&'static str),
|
|
|
|
/// Represents an invalid subscription key.
|
|
#[error("Invalid Subscription Key format")]
|
|
InvalidSubscriptionKey,
|
|
|
|
/// Indicates that conflicting parameters were supplied.
|
|
#[error("Cannot provide conflicting parameters: {0}")]
|
|
ConflictingParameters(&'static str),
|
|
|
|
/// Wraps a standard I/O error.
|
|
#[error("I/O Error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
|
|
/// Represents an error while parsing a header's value.
|
|
#[error("Header value parse error for '{0}': {1}")]
|
|
HeaderValueParse(&'static str, String),
|
|
|
|
/// Indicates an invalid parameter was provided.
|
|
#[error("Invalid parameter value: {0}")]
|
|
InvalidParameter(&'static str),
|
|
|
|
/// Indicates that the API rate limit has been exceeded (HTTP 429).
|
|
///
|
|
/// This error is returned after the client has exhausted all automatic retry attempts
|
|
/// for a rate-limited request. The `retry_after` field contains the number of seconds
|
|
/// that was specified in the last Retry-After header. By default, the client automatically
|
|
/// retries rate-limited requests up to 3 times.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```rust,no_run
|
|
/// use intel_dcap_api::{ApiClient, IntelApiError};
|
|
///
|
|
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
|
|
/// let mut client = ApiClient::new()?;
|
|
/// client.set_max_retries(0); // Disable automatic retries
|
|
///
|
|
/// match client.get_sgx_tcb_info("00606A000000", None, None).await {
|
|
/// Ok(tcb_info) => println!("Success"),
|
|
/// Err(IntelApiError::TooManyRequests { request_id, retry_after }) => {
|
|
/// println!("Rate limited after all retries. Last retry-after was {} seconds.", retry_after);
|
|
/// }
|
|
/// Err(e) => eprintln!("Other error: {}", e),
|
|
/// }
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[error("Too many requests. Retry after {retry_after} seconds")]
|
|
TooManyRequests {
|
|
/// The unique request identifier for tracing.
|
|
request_id: String,
|
|
/// Number of seconds to wait before retrying, from Retry-After header.
|
|
retry_after: u64,
|
|
},
|
|
}
|
|
|
|
/// Extracts common API error details from response headers.
|
|
pub(crate) fn extract_api_error_details(
|
|
response: &Response,
|
|
) -> (String, Option<String>, Option<String>) {
|
|
let request_id = response
|
|
.headers()
|
|
.get("Request-ID")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("Unknown")
|
|
.to_string();
|
|
let error_code = response
|
|
.headers()
|
|
.get("Error-Code")
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(String::from);
|
|
let error_message = response
|
|
.headers()
|
|
.get("Error-Message")
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(String::from);
|
|
(request_id, error_code, error_message)
|
|
}
|
|
|
|
/// Checks the response status and returns an ApiError if it's not one of the expected statuses.
|
|
pub(crate) async fn check_status(
|
|
response: Response,
|
|
expected_statuses: &[StatusCode],
|
|
) -> Result<Response, IntelApiError> {
|
|
let status = response.status();
|
|
if expected_statuses.contains(&status) {
|
|
Ok(response)
|
|
} else if status == StatusCode::TOO_MANY_REQUESTS {
|
|
// Handle 429 Too Many Requests with Retry-After header
|
|
let request_id = response
|
|
.headers()
|
|
.get("Request-ID")
|
|
.and_then(|v| v.to_str().ok())
|
|
.unwrap_or("Unknown")
|
|
.to_string();
|
|
|
|
// Parse Retry-After header (can be in seconds or HTTP date format)
|
|
let retry_after = response
|
|
.headers()
|
|
.get("Retry-After")
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(|v| v.parse::<u64>().ok())
|
|
.unwrap_or(60); // Default to 60 seconds if header is missing or invalid
|
|
|
|
Err(IntelApiError::TooManyRequests {
|
|
request_id,
|
|
retry_after,
|
|
})
|
|
} else {
|
|
let (request_id, error_code, error_message) = extract_api_error_details(&response);
|
|
Err(IntelApiError::ApiError {
|
|
status,
|
|
request_id,
|
|
error_code,
|
|
error_message,
|
|
})
|
|
}
|
|
}
|