mirror of
https://github.com/matter-labs/teepot.git
synced 2025-07-21 23:23:57 +02:00
feat(intel-dcap-api): add automatic retry logic for 429 rate limiting
- 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>
This commit is contained in:
parent
205113ecfa
commit
bb9c5b195e
9 changed files with 267 additions and 15 deletions
|
@ -46,7 +46,7 @@ impl ApiClient {
|
|||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
let response = request_builder.send().await?;
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let fmspcs_json = response.text().await?;
|
||||
|
|
|
@ -12,6 +12,8 @@ use crate::{
|
|||
use percent_encoding::percent_decode_str;
|
||||
use reqwest::{RequestBuilder, Response, StatusCode};
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
impl ApiClient {
|
||||
/// Helper to construct API paths dynamically based on version and technology (SGX/TDX).
|
||||
|
@ -84,7 +86,7 @@ impl ApiClient {
|
|||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
) -> Result<PckCertificateResponse, IntelApiError> {
|
||||
let response = request_builder.send().await?;
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain = self.get_required_header(
|
||||
|
@ -109,7 +111,7 @@ impl ApiClient {
|
|||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
) -> Result<PckCertificatesResponse, IntelApiError> {
|
||||
let response = request_builder.send().await?;
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain = self.get_required_header(
|
||||
|
@ -134,7 +136,7 @@ impl ApiClient {
|
|||
v4_issuer_chain_header: &'static str,
|
||||
v3_issuer_chain_header: Option<&'static str>,
|
||||
) -> Result<(String, String), IntelApiError> {
|
||||
let response = request_builder.send().await?;
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain =
|
||||
|
@ -158,7 +160,7 @@ impl ApiClient {
|
|||
))
|
||||
})?;
|
||||
|
||||
let response = builder_clone.send().await?;
|
||||
let response = self.execute_with_retry(builder_clone).await?;
|
||||
let status = response.status();
|
||||
|
||||
if status == StatusCode::NOT_FOUND || status == StatusCode::GONE {
|
||||
|
@ -224,4 +226,69 @@ impl ApiClient {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a request with automatic retry logic for rate limiting (429 responses).
|
||||
///
|
||||
/// This method will automatically retry the request up to `max_retries` times
|
||||
/// when receiving a 429 Too Many Requests response, waiting for the duration
|
||||
/// specified in the Retry-After header.
|
||||
pub(super) async fn execute_with_retry(
|
||||
&self,
|
||||
request_builder: RequestBuilder,
|
||||
) -> Result<Response, IntelApiError> {
|
||||
let mut retries = 0;
|
||||
|
||||
loop {
|
||||
// Clone the request builder for retry attempts
|
||||
let builder = request_builder.try_clone().ok_or_else(|| {
|
||||
IntelApiError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to clone request builder for retry",
|
||||
))
|
||||
})?;
|
||||
|
||||
let response = builder.send().await?;
|
||||
let status = response.status();
|
||||
|
||||
if status != StatusCode::TOO_MANY_REQUESTS {
|
||||
// Not a rate limit error, return the response
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
// Handle 429 Too Many Requests
|
||||
if retries >= self.max_retries {
|
||||
// No more retries, return the error
|
||||
let request_id = response
|
||||
.headers()
|
||||
.get("Request-ID")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
|
||||
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);
|
||||
|
||||
return Err(IntelApiError::TooManyRequests {
|
||||
request_id,
|
||||
retry_after,
|
||||
});
|
||||
}
|
||||
|
||||
// Parse Retry-After header
|
||||
let retry_after_secs = 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
|
||||
|
||||
// Wait before retrying
|
||||
sleep(Duration::from_secs(retry_after_secs)).await;
|
||||
retries += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ pub struct ApiClient {
|
|||
client: Client,
|
||||
base_url: Url,
|
||||
api_version: ApiVersion,
|
||||
/// Maximum number of automatic retries for rate-limited requests (429 responses)
|
||||
max_retries: u32,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
|
@ -114,6 +116,20 @@ impl ApiClient {
|
|||
.build()?,
|
||||
base_url: base_url.into_url()?,
|
||||
api_version,
|
||||
max_retries: 3, // Default to 3 retries
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the maximum number of automatic retries for rate-limited requests.
|
||||
///
|
||||
/// When the API returns a 429 (Too Many Requests) response, the client will
|
||||
/// automatically wait for the duration specified in the Retry-After header
|
||||
/// and retry the request up to this many times.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `max_retries` - Maximum number of retries (0 disables automatic retries)
|
||||
pub fn set_max_retries(&mut self, max_retries: u32) {
|
||||
self.max_retries = max_retries;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ impl ApiClient {
|
|||
}
|
||||
|
||||
let request_builder = self.client.get(url);
|
||||
let response = request_builder.send().await?;
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
let issuer_chain = self.get_required_header(
|
||||
|
|
|
@ -36,13 +36,13 @@ impl ApiClient {
|
|||
let path = self.build_api_path("sgx", "registration", "platform")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
|
||||
let response = self
|
||||
let request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(platform_manifest)
|
||||
.send()
|
||||
.await?;
|
||||
.body(platform_manifest);
|
||||
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
|
||||
let response = check_status(response, &[StatusCode::CREATED]).await?;
|
||||
|
||||
|
@ -81,14 +81,14 @@ impl ApiClient {
|
|||
let path = self.build_api_path("sgx", "registration", "package")?;
|
||||
let url = self.base_url.join(&path)?;
|
||||
|
||||
let response = self
|
||||
let request_builder = self
|
||||
.client
|
||||
.post(url)
|
||||
.header("Ocp-Apim-Subscription-Key", subscription_key)
|
||||
.header(header::CONTENT_TYPE, "application/octet-stream")
|
||||
.body(add_package_request)
|
||||
.send()
|
||||
.await?;
|
||||
.body(add_package_request);
|
||||
|
||||
let response = self.execute_with_retry(request_builder).await?;
|
||||
|
||||
let response = check_status(response, &[StatusCode::OK]).await?;
|
||||
|
||||
|
|
|
@ -59,6 +59,40 @@ pub enum IntelApiError {
|
|||
/// 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.
|
||||
|
@ -92,6 +126,27 @@ pub(crate) async fn check_status(
|
|||
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 {
|
||||
|
|
|
@ -8,6 +8,14 @@
|
|||
//!
|
||||
//! Create an [`ApiClient`] to interface with the Intel API.
|
||||
//!
|
||||
//! # Rate Limiting
|
||||
//!
|
||||
//! The Intel API implements rate limiting and may return HTTP 429 (Too Many Requests) responses.
|
||||
//! This client automatically handles rate limiting by retrying requests up to 3 times by default,
|
||||
//! waiting for the duration specified in the `Retry-After` header. You can configure the retry
|
||||
//! behavior using [`ApiClient::set_max_retries`]. If all retries are exhausted, the client
|
||||
//! returns an [`IntelApiError::TooManyRequests`] error.
|
||||
//!
|
||||
//! Example
|
||||
//! ```rust,no_run
|
||||
//! use intel_dcap_api::{ApiClient, IntelApiError, TcbInfoResponse};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue