// 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, /// An optional server-provided error message. error_message: Option, }, /// 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> { /// 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, Option) { 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 { 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 { status, request_id, error_code, error_message, }) } }