From bb9c5b195e59a87f656814c0b1acc0e9268f3395 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 28 May 2025 11:00:03 +0200 Subject: [PATCH] feat(intel-dcap-api): add automatic retry logic for 429 rate limiting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Signed-off-by: Harald Hoyer --- crates/intel-dcap-api/CLAUDE.md | 17 +++- .../examples/handle_rate_limit.rs | 91 +++++++++++++++++++ crates/intel-dcap-api/src/client/fmspc.rs | 2 +- crates/intel-dcap-api/src/client/helpers.rs | 75 ++++++++++++++- crates/intel-dcap-api/src/client/mod.rs | 16 ++++ crates/intel-dcap-api/src/client/pck_crl.rs | 2 +- .../intel-dcap-api/src/client/registration.rs | 16 ++-- crates/intel-dcap-api/src/error.rs | 55 +++++++++++ crates/intel-dcap-api/src/lib.rs | 8 ++ 9 files changed, 267 insertions(+), 15 deletions(-) create mode 100644 crates/intel-dcap-api/examples/handle_rate_limit.rs diff --git a/crates/intel-dcap-api/CLAUDE.md b/crates/intel-dcap-api/CLAUDE.md index d2a01b3..851b6ae 100644 --- a/crates/intel-dcap-api/CLAUDE.md +++ b/crates/intel-dcap-api/CLAUDE.md @@ -16,6 +16,7 @@ and enclave identity verification. - Type-safe request/response structures - Support for SGX and TDX platforms - Real data integration tests +- **Automatic rate limit handling with configurable retries** ## Development Commands @@ -36,6 +37,7 @@ cargo run --example get_pck_crl # Fetch certificate revocation lists cargo run --example common_usage # Common attestation verification patterns cargo run --example integration_test # Comprehensive test of most API endpoints cargo run --example fetch_test_data # Fetch real data from Intel API for tests +cargo run --example handle_rate_limit # Demonstrate automatic rate limiting handling ``` ## Architecture @@ -45,6 +47,8 @@ cargo run --example fetch_test_data # Fetch real data from Intel API for tests - **ApiClient** (`src/client/mod.rs`): Main entry point supporting API v3/v4 - Base URL: https://api.trustedservices.intel.com - Manages HTTP client and API version selection + - Automatic retry logic for 429 (Too Many Requests) responses + - Default: 3 retries, configurable via `set_max_retries()` ### Key Modules @@ -69,6 +73,7 @@ cargo run --example fetch_test_data # Fetch real data from Intel API for tests - **error.rs**: `IntelApiError` for comprehensive error handling - Extracts error details from Error-Code and Error-Message headers + - **`TooManyRequests` variant for rate limiting (429) after retry exhaustion** - **types.rs**: Enums (CaType, ApiVersion, UpdateType, etc.) - **requests.rs**: Request structures - **responses.rs**: Response structures with JSON and certificate data @@ -78,10 +83,18 @@ cargo run --example fetch_test_data # Fetch real data from Intel API for tests All client methods follow this pattern: 1. Build request with query parameters -2. Send HTTP request with proper headers +2. Send HTTP request with proper headers (with automatic retry on 429) 3. Parse response (JSON + certificate chains) 4. Return typed response or error +### Rate Limiting & Retry Logic + +- **Automatic Retries**: All HTTP requests automatically retry on 429 (Too Many Requests) responses +- **Retry Configuration**: Default 3 retries, configurable via `ApiClient::set_max_retries()` +- **Retry-After Handling**: Waits for duration specified in Retry-After header before retrying +- **Error Handling**: `IntelApiError::TooManyRequests` returned only after all retries exhausted +- **Implementation**: `execute_with_retry()` in `src/client/helpers.rs` handles retry logic + ### Testing Strategy - **Mock Tests**: Two test suites using mockito for HTTP mocking @@ -114,6 +127,8 @@ All client methods follow this pattern: 1. **Mockito Header Encoding**: Always URL-encode headers containing newlines/special characters 2. **API Version Selection**: Some endpoints are V4-only and will return errors on V3 +3. **Rate Limiting**: Client automatically retries 429 responses; disable with `set_max_retries(0)` if manual handling + needed 4. **Platform Filters**: Only certain values are valid (All, Client, E3, E5) 5. **Test Data**: PCK certificate endpoints require valid platform data and often need subscription keys 6. **Issuer Chain Validation**: Always check that `issuer_chain` is non-empty - it's critical for signature verification diff --git a/crates/intel-dcap-api/examples/handle_rate_limit.rs b/crates/intel-dcap-api/examples/handle_rate_limit.rs new file mode 100644 index 0000000..def5af6 --- /dev/null +++ b/crates/intel-dcap-api/examples/handle_rate_limit.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2025 Matter Labs + +//! Example demonstrating automatic rate limit handling +//! +//! The Intel DCAP API client now automatically handles 429 Too Many Requests responses +//! by retrying up to 3 times by default. This example shows how to configure the retry +//! behavior and handle cases where all retries are exhausted. + +use intel_dcap_api::{ApiClient, IntelApiError}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create API client with default settings (3 retries) + let mut client = ApiClient::new()?; + + println!("Example 1: Default behavior (automatic retries)"); + println!("================================================"); + + // Example FMSPC value + let fmspc = "00606A000000"; + + // The client will automatically retry up to 3 times if rate limited + match client.get_sgx_tcb_info(fmspc, None, None).await { + Ok(tcb_info) => { + println!("✓ Successfully retrieved TCB info"); + println!( + " TCB Info JSON length: {} bytes", + tcb_info.tcb_info_json.len() + ); + println!( + " Issuer Chain length: {} bytes", + tcb_info.issuer_chain.len() + ); + } + Err(IntelApiError::TooManyRequests { + request_id, + retry_after, + }) => { + println!("✗ Rate limited even after 3 automatic retries"); + println!(" Request ID: {}", request_id); + println!(" Last retry-after was: {} seconds", retry_after); + } + Err(e) => { + eprintln!("✗ Other error: {}", e); + } + } + + println!("\nExample 2: Custom retry configuration"); + println!("====================================="); + + // Configure client to retry up to 5 times + client.set_max_retries(5); + println!("Set max retries to 5"); + + match client.get_sgx_tcb_info(fmspc, None, None).await { + Ok(_) => println!("✓ Request succeeded"), + Err(IntelApiError::TooManyRequests { .. }) => { + println!("✗ Still rate limited after 5 retries") + } + Err(e) => eprintln!("✗ Error: {}", e), + } + + println!("\nExample 3: Disable automatic retries"); + println!("===================================="); + + // Disable automatic retries + client.set_max_retries(0); + println!("Disabled automatic retries"); + + match client.get_sgx_tcb_info(fmspc, None, None).await { + Ok(_) => println!("✓ Request succeeded on first attempt"), + Err(IntelApiError::TooManyRequests { + request_id, + retry_after, + }) => { + println!("✗ Rate limited (no automatic retry)"); + println!(" Request ID: {}", request_id); + println!(" Retry after: {} seconds", retry_after); + println!(" You would need to implement manual retry logic here"); + } + Err(e) => eprintln!("✗ Error: {}", e), + } + + println!("\nNote: The client handles rate limiting automatically!"); + println!("You only need to handle TooManyRequests errors if:"); + println!("- You disable automatic retries (set_max_retries(0))"); + println!("- All automatic retries are exhausted"); + + Ok(()) +} diff --git a/crates/intel-dcap-api/src/client/fmspc.rs b/crates/intel-dcap-api/src/client/fmspc.rs index 3963f1f..c515ac9 100644 --- a/crates/intel-dcap-api/src/client/fmspc.rs +++ b/crates/intel-dcap-api/src/client/fmspc.rs @@ -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?; diff --git a/crates/intel-dcap-api/src/client/helpers.rs b/crates/intel-dcap-api/src/client/helpers.rs index ee53cc3..ba86545 100644 --- a/crates/intel-dcap-api/src/client/helpers.rs +++ b/crates/intel-dcap-api/src/client/helpers.rs @@ -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 { - 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 { - 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 { + 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::().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::().ok()) + .unwrap_or(60); // Default to 60 seconds + + // Wait before retrying + sleep(Duration::from_secs(retry_after_secs)).await; + retries += 1; + } + } } diff --git a/crates/intel-dcap-api/src/client/mod.rs b/crates/intel-dcap-api/src/client/mod.rs index d2d618d..3e71d5d 100644 --- a/crates/intel-dcap-api/src/client/mod.rs +++ b/crates/intel-dcap-api/src/client/mod.rs @@ -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; + } } diff --git a/crates/intel-dcap-api/src/client/pck_crl.rs b/crates/intel-dcap-api/src/client/pck_crl.rs index 0226b11..d5d0c6e 100644 --- a/crates/intel-dcap-api/src/client/pck_crl.rs +++ b/crates/intel-dcap-api/src/client/pck_crl.rs @@ -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( diff --git a/crates/intel-dcap-api/src/client/registration.rs b/crates/intel-dcap-api/src/client/registration.rs index 136c8c0..d510649 100644 --- a/crates/intel-dcap-api/src/client/registration.rs +++ b/crates/intel-dcap-api/src/client/registration.rs @@ -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?; diff --git a/crates/intel-dcap-api/src/error.rs b/crates/intel-dcap-api/src/error.rs index 564bbd2..4e07216 100644 --- a/crates/intel-dcap-api/src/error.rs +++ b/crates/intel-dcap-api/src/error.rs @@ -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> { + /// 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::().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 { diff --git a/crates/intel-dcap-api/src/lib.rs b/crates/intel-dcap-api/src/lib.rs index a0d9e20..f6f4ea4 100644 --- a/crates/intel-dcap-api/src/lib.rs +++ b/crates/intel-dcap-api/src/lib.rs @@ -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};