intel_dcap_api/error.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 Matter Labs
3
4use reqwest::{Response, StatusCode};
5use thiserror::Error;
6
7/// Represents all possible errors that can occur when interacting with Intel's DCAP API.
8#[derive(Error, Debug)]
9pub enum IntelApiError {
10 /// Indicates that the requested API version or feature is unsupported.
11 #[error("Unsupported API version or feature: {0}")]
12 UnsupportedApiVersion(String),
13
14 /// Wraps an underlying reqwest error.
15 #[error("Reqwest error: {0}")]
16 Reqwest(#[from] reqwest::Error),
17
18 /// Wraps a URL parsing error.
19 #[error("URL parsing error: {0}")]
20 UrlParse(#[from] url::ParseError),
21
22 /// Wraps a Serde JSON error.
23 #[error("Serde JSON error: {0}")]
24 JsonError(#[from] serde_json::Error),
25
26 /// Represents a general API error, capturing the HTTP status and optional error details.
27 #[error("API Error: Status={status}, Request-ID={request_id}, Code={error_code:?}, Message={error_message:?}")]
28 ApiError {
29 /// HTTP status code returned by the API.
30 status: StatusCode,
31 /// The unique request identifier for tracing errors.
32 request_id: String,
33 /// An optional server-provided error code.
34 error_code: Option<String>,
35 /// An optional server-provided error message.
36 error_message: Option<String>,
37 },
38
39 /// Indicates that a header is missing or invalid.
40 #[error("Header missing or invalid: {0}")]
41 MissingOrInvalidHeader(&'static str),
42
43 /// Represents an invalid subscription key.
44 #[error("Invalid Subscription Key format")]
45 InvalidSubscriptionKey,
46
47 /// Indicates that conflicting parameters were supplied.
48 #[error("Cannot provide conflicting parameters: {0}")]
49 ConflictingParameters(&'static str),
50
51 /// Wraps a standard I/O error.
52 #[error("I/O Error: {0}")]
53 Io(#[from] std::io::Error),
54
55 /// Represents an error while parsing a header's value.
56 #[error("Header value parse error for '{0}': {1}")]
57 HeaderValueParse(&'static str, String),
58
59 /// Indicates an invalid parameter was provided.
60 #[error("Invalid parameter value: {0}")]
61 InvalidParameter(&'static str),
62
63 /// Indicates that the API rate limit has been exceeded (HTTP 429).
64 ///
65 /// This error is returned after the client has exhausted all automatic retry attempts
66 /// for a rate-limited request. The `retry_after` field contains the number of seconds
67 /// that was specified in the last Retry-After header. By default, the client automatically
68 /// retries rate-limited requests up to 3 times.
69 ///
70 /// # Example
71 ///
72 /// ```rust,no_run
73 /// use intel_dcap_api::{ApiClient, IntelApiError};
74 ///
75 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
76 /// let mut client = ApiClient::new()?;
77 /// client.set_max_retries(0); // Disable automatic retries
78 ///
79 /// match client.get_sgx_tcb_info("00606A000000", None, None).await {
80 /// Ok(tcb_info) => println!("Success"),
81 /// Err(IntelApiError::TooManyRequests { request_id, retry_after }) => {
82 /// println!("Rate limited after all retries. Last retry-after was {} seconds.", retry_after);
83 /// }
84 /// Err(e) => eprintln!("Other error: {}", e),
85 /// }
86 /// # Ok(())
87 /// # }
88 /// ```
89 #[error("Too many requests. Retry after {retry_after} seconds")]
90 TooManyRequests {
91 /// The unique request identifier for tracing.
92 request_id: String,
93 /// Number of seconds to wait before retrying, from Retry-After header.
94 retry_after: u64,
95 },
96}
97
98/// Extracts common API error details from response headers.
99pub(crate) fn extract_api_error_details(
100 response: &Response,
101) -> (String, Option<String>, Option<String>) {
102 let request_id = response
103 .headers()
104 .get("Request-ID")
105 .and_then(|v| v.to_str().ok())
106 .unwrap_or("Unknown")
107 .to_string();
108 let error_code = response
109 .headers()
110 .get("Error-Code")
111 .and_then(|v| v.to_str().ok())
112 .map(String::from);
113 let error_message = response
114 .headers()
115 .get("Error-Message")
116 .and_then(|v| v.to_str().ok())
117 .map(String::from);
118 (request_id, error_code, error_message)
119}
120
121/// Checks the response status and returns an ApiError if it's not one of the expected statuses.
122pub(crate) async fn check_status(
123 response: Response,
124 expected_statuses: &[StatusCode],
125) -> Result<Response, IntelApiError> {
126 let status = response.status();
127 if expected_statuses.contains(&status) {
128 Ok(response)
129 } else if status == StatusCode::TOO_MANY_REQUESTS {
130 // Handle 429 Too Many Requests with Retry-After header
131 let request_id = response
132 .headers()
133 .get("Request-ID")
134 .and_then(|v| v.to_str().ok())
135 .unwrap_or("Unknown")
136 .to_string();
137
138 // Parse Retry-After header (can be in seconds or HTTP date format)
139 let retry_after = response
140 .headers()
141 .get("Retry-After")
142 .and_then(|v| v.to_str().ok())
143 .and_then(|v| v.parse::<u64>().ok())
144 .unwrap_or(60); // Default to 60 seconds if header is missing or invalid
145
146 Err(IntelApiError::TooManyRequests {
147 request_id,
148 retry_after,
149 })
150 } else {
151 let (request_id, error_code, error_message) = extract_api_error_details(&response);
152 Err(IntelApiError::ApiError {
153 status,
154 request_id,
155 error_code,
156 error_message,
157 })
158 }
159}