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:
Harald Hoyer 2025-05-28 11:00:03 +02:00
parent 205113ecfa
commit bb9c5b195e
Signed by: harald
GPG key ID: F519A1143B3FBE32
9 changed files with 267 additions and 15 deletions

View file

@ -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?;

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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(

View file

@ -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?;

View file

@ -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 {

View file

@ -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};