Skip to main content

oci_rust_sdk/core/client/
http_client.rs

1use crate::auth::provider::AuthProvider;
2use crate::auth::signer::RequestSigner;
3use crate::core::error::{OciError, ServiceErrorResponse};
4use crate::core::retry::Retrier;
5use reqwest::Method;
6use serde::de::DeserializeOwned;
7use std::sync::Arc;
8
9/// OCI API client for making requests to Oracle Cloud Infrastructure services
10pub struct OciClient {
11    client: reqwest::Client,
12    signer: RequestSigner,
13    endpoint: String,
14    retrier: Retrier,
15}
16
17impl OciClient {
18    /// Create a new OCI client
19    ///
20    /// # Arguments
21    /// * `auth_provider` - Authentication provider
22    /// * `endpoint` - Base endpoint URL (e.g., "https://osmh.ap-seoul-1.oci.oraclecloud.com")
23    pub fn new(
24        auth_provider: Arc<dyn AuthProvider>,
25        endpoint: String,
26    ) -> crate::core::Result<Self> {
27        Self::with_timeout(auth_provider, endpoint, std::time::Duration::from_secs(30))
28    }
29
30    pub fn with_timeout(
31        auth_provider: Arc<dyn AuthProvider>,
32        endpoint: String,
33        timeout: std::time::Duration,
34    ) -> crate::core::Result<Self> {
35        let client = reqwest::Client::builder()
36            .user_agent(format!("oci-rust-sdk/{}", env!("CARGO_PKG_VERSION")))
37            .timeout(timeout)
38            .gzip(true)
39            .build()
40            .map_err(OciError::HttpError)?;
41
42        let signer = RequestSigner::new(auth_provider)?;
43        let retrier = Retrier::new();
44
45        Ok(Self {
46            client,
47            signer,
48            endpoint,
49            retrier,
50        })
51    }
52
53    /// Create a new client with custom retry configuration
54    pub fn with_retrier(mut self, retrier: Retrier) -> Self {
55        self.retrier = retrier;
56        self
57    }
58
59    /// Make a GET request
60    pub async fn get<T>(&self, path: &str) -> crate::core::Result<OciResponse<T>>
61    where
62        T: DeserializeOwned,
63    {
64        self.request(Method::GET, path, None::<&()>).await
65    }
66
67    /// Make a POST request
68    pub async fn post<B, T>(
69        &self,
70        path: &str,
71        body: Option<&B>,
72    ) -> crate::core::Result<OciResponse<T>>
73    where
74        B: serde::Serialize,
75        T: DeserializeOwned,
76    {
77        self.request(Method::POST, path, body).await
78    }
79
80    /// Make a PUT request
81    pub async fn put<B, T>(
82        &self,
83        path: &str,
84        body: Option<&B>,
85    ) -> crate::core::Result<OciResponse<T>>
86    where
87        B: serde::Serialize,
88        T: DeserializeOwned,
89    {
90        self.request(Method::PUT, path, body).await
91    }
92
93    /// Make a DELETE request
94    pub async fn delete<T>(&self, path: &str) -> crate::core::Result<OciResponse<T>>
95    where
96        T: DeserializeOwned,
97    {
98        self.request(Method::DELETE, path, None::<&()>).await
99    }
100
101    /// Make an HTTP request with retry logic
102    async fn request<B, T>(
103        &self,
104        method: Method,
105        path: &str,
106        body: Option<&B>,
107    ) -> crate::core::Result<OciResponse<T>>
108    where
109        B: serde::Serialize,
110        T: DeserializeOwned,
111    {
112        // Serialize body once (outside retry loop)
113        let body_bytes = if let Some(b) = body {
114            Some(serde_json::to_vec(b)?)
115        } else {
116            None
117        };
118
119        // Execute with retry
120        self.retrier
121            .execute_with_retry(|| {
122                let body_ref = body_bytes.as_deref();
123                self.execute_request(method.clone(), path, body_ref)
124            })
125            .await
126    }
127
128    /// Execute a single HTTP request (called by retry logic)
129    async fn execute_request<T>(
130        &self,
131        method: Method,
132        path: &str,
133        body: Option<&[u8]>,
134    ) -> crate::core::Result<OciResponse<T>>
135    where
136        T: DeserializeOwned,
137    {
138        // Build URL
139        let url = format!("{}{}", self.endpoint, path);
140        let parsed_url =
141            url::Url::parse(&url).map_err(|e| OciError::Other(format!("Invalid URL: {}", e)))?;
142
143        // Create request builder
144        let mut request_builder = self.client.request(method.clone(), url);
145
146        // Add body if present
147        if let Some(body_bytes) = body {
148            request_builder = request_builder.body(body_bytes.to_vec());
149        }
150
151        // Build request to get headers
152        let mut request = request_builder.build().map_err(OciError::HttpError)?;
153
154        // Sign the request
155        self.signer
156            .sign_request(method.as_str(), &parsed_url, request.headers_mut(), body)?;
157
158        // Execute request
159        let response = self.client.execute(request).await?;
160
161        // Check status
162        let status = response.status();
163        let headers = response.headers().clone();
164
165        if !status.is_success() {
166            // Parse error response
167            let error_text = response.text().await?;
168
169            let error = if let Ok(service_error) =
170                serde_json::from_str::<ServiceErrorResponse>(&error_text)
171            {
172                OciError::from_response(status.as_u16(), service_error.code, service_error.message)
173            } else {
174                OciError::from_response(status.as_u16(), "Unknown".to_string(), error_text)
175            };
176
177            return Err(error);
178        }
179
180        // Parse response body
181        let body = if status == reqwest::StatusCode::NO_CONTENT {
182            // No content to parse
183            serde_json::from_str("{}")?
184        } else {
185            let bytes = match response.bytes().await {
186                Ok(b) => b,
187                Err(e) => {
188                    eprintln!("OCI_DIAG body.bytes() failed: {:?}", e);
189                    return Err(OciError::HttpError(e));
190                }
191            };
192            match serde_json::from_slice(&bytes) {
193                Ok(body) => body,
194                Err(e) => {
195                    let preview: String =
196                        String::from_utf8_lossy(&bytes).chars().take(2000).collect();
197                    eprintln!(
198                        "OCI_DIAG json parse failed: {} | len={} | body_preview={}",
199                        e,
200                        bytes.len(),
201                        preview
202                    );
203                    return Err(OciError::SerdeError(e));
204                }
205            }
206        };
207
208        Ok(OciResponse { body, headers })
209    }
210}
211
212/// HTTP response from OCI service
213pub struct OciResponse<T> {
214    pub body: T,
215    pub headers: reqwest::header::HeaderMap,
216}
217
218impl<T> OciResponse<T> {
219    /// Get a header value as a string
220    pub fn get_header(&self, name: &str) -> Option<String> {
221        self.headers
222            .get(name)
223            .and_then(|v| v.to_str().ok())
224            .map(|s| s.to_string())
225    }
226
227    /// Get opc-request-id header
228    pub fn request_id(&self) -> Option<String> {
229        self.get_header("opc-request-id")
230    }
231
232    /// Get opc-next-page header (for pagination)
233    pub fn next_page(&self) -> Option<String> {
234        self.get_header("opc-next-page")
235    }
236}