Skip to main content

cloud_sdk_reqwest/asynchronous/
client.rs

1use core::fmt;
2use std::sync::Arc;
3
4use cloud_sdk::Method;
5use cloud_sdk::transport::{
6    AsyncTransport, BoundTransport, EndpointIdentity, EndpointIdentityError, StatusCode,
7    TransportRequest, TransportResponse,
8};
9use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes};
10use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderValue};
11use reqwest::{Body, Client};
12
13use crate::shared::{
14    BearerToken, CredentialStateError, CredentialStore, HttpsEndpoint, TokenRotationError,
15    TransportError, parse_rate_limit,
16};
17
18use super::body::SanitizedBuffer;
19
20/// Hardened provider-neutral reqwest asynchronous transport.
21///
22/// The adapter uses reqwest's Tokio-based execution internally but does not
23/// install or own a runtime. Callers must poll it from a compatible executor.
24#[derive(Clone)]
25pub struct AsyncClient {
26    client: Client,
27    endpoint: HttpsEndpoint,
28    credentials: Arc<CredentialStore>,
29}
30
31impl AsyncClient {
32    pub(super) fn new(client: Client, endpoint: HttpsEndpoint, token: BearerToken) -> Self {
33        Self {
34            client,
35            endpoint,
36            credentials: Arc::new(CredentialStore::new(token)),
37        }
38    }
39
40    /// Atomically replaces the bearer token used by newly started requests.
41    ///
42    /// In-flight requests retain their previous snapshot. No credential lock
43    /// is held across network I/O or `.await`.
44    pub fn rotate_bearer_token(
45        &self,
46        replacement: BearerToken,
47    ) -> Result<(), CredentialStateError> {
48        self.credentials.rotate(replacement)
49    }
50
51    /// Validates and rotates from mutable bytes, clearing the complete source
52    /// on success or failure. Rejected input leaves the active token unchanged.
53    pub fn rotate_bearer_token_from_mut_bytes(
54        &self,
55        source: &mut [u8],
56    ) -> Result<(), TokenRotationError> {
57        self.credentials.rotate_from_mut_bytes(source)
58    }
59
60    /// Validates and rotates from guarded storage. Dropping the consumed guard
61    /// clears the complete source on success or failure.
62    pub fn rotate_bearer_token_from_secret_buffer(
63        &self,
64        source: SecretBuffer<'_>,
65    ) -> Result<(), TokenRotationError> {
66        self.credentials.rotate_from_secret_buffer(source)
67    }
68
69    async fn send_inner<'buffer>(
70        &self,
71        request: TransportRequest<'_>,
72        response_body: &'buffer mut [u8],
73    ) -> Result<TransportResponse<'buffer>, TransportError> {
74        sanitize_bytes(response_body);
75        let url = self
76            .endpoint
77            .compose(request.target())
78            .map_err(|_| TransportError::TargetRejected)?;
79        let token_snapshot = self
80            .credentials
81            .snapshot()
82            .map_err(|_| TransportError::CredentialStateUnavailable)?;
83        let authorization = token_snapshot
84            .header_value()
85            .map_err(|_| TransportError::HeaderRejected)?;
86        let mut outbound = self
87            .client
88            .request(map_method(request.method()), url)
89            .header(AUTHORIZATION, authorization)
90            .header(ACCEPT, HeaderValue::from_static("application/json"));
91
92        if let Some(content_type) = request.content_type() {
93            let value = HeaderValue::from_str(content_type.as_str())
94                .map_err(|_| TransportError::HeaderRejected)?;
95            outbound = outbound.header(CONTENT_TYPE, value);
96        } else if !request.body().is_empty() {
97            return Err(TransportError::MissingContentType);
98        }
99
100        if !request.body().is_empty() {
101            let body = SanitizedBuffer::copy_from(request.body())
102                .map_err(|_| TransportError::RequestBodyAllocationFailed)?;
103            let _ = u64::try_from(request.body().len())
104                .map_err(|_| TransportError::RequestBodyTooLarge)?;
105            outbound = outbound.body(Body::from(body.into_bytes()));
106        }
107
108        let mut response = outbound.send().await.map_err(classify_reqwest_error)?;
109        self.endpoint
110            .verify_origin(response.url())
111            .map_err(|_| TransportError::ResponseOriginChanged)?;
112        if response.content_length().is_some_and(|length| {
113            u64::try_from(response_body.len()).map_or(true, |cap| length > cap)
114        }) {
115            return Err(TransportError::ResponseTooLarge);
116        }
117        let status =
118            StatusCode::new(response.status().as_u16()).ok_or(TransportError::InvalidStatus)?;
119        let rate_limit = parse_rate_limit(response.headers())?;
120        let buffered = read_response(&mut response, response_body.len()).await?;
121        let body_len = buffered.len();
122        let initialized = response_body
123            .get_mut(..body_len)
124            .ok_or(TransportError::ResponseReadFailed)?;
125        initialized.copy_from_slice(buffered.as_ref());
126        let response = TransportResponse::new(status, initialized);
127        drop(token_snapshot);
128        Ok(rate_limit.map_or(response, |value| response.with_rate_limit(value)))
129    }
130}
131
132impl AsyncTransport for AsyncClient {
133    type Error = TransportError;
134
135    async fn send<'transport, 'request, 'buffer>(
136        &'transport self,
137        request: TransportRequest<'request>,
138        response_body: &'buffer mut [u8],
139    ) -> Result<TransportResponse<'buffer>, Self::Error>
140    where
141        'request: 'transport,
142        'buffer: 'transport,
143    {
144        self.send_inner(request, response_body).await
145    }
146}
147
148impl BoundTransport for AsyncClient {
149    fn endpoint_identity(&self) -> Result<EndpointIdentity<'_>, EndpointIdentityError> {
150        self.endpoint.identity()
151    }
152}
153
154impl fmt::Debug for AsyncClient {
155    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156        formatter
157            .debug_struct("AsyncClient")
158            .field("endpoint", &"[redacted]")
159            .field("credentials", &"[redacted]")
160            .finish_non_exhaustive()
161    }
162}
163
164async fn read_response(
165    response: &mut reqwest::Response,
166    limit: usize,
167) -> Result<SanitizedBuffer, TransportError> {
168    let mut buffered = SanitizedBuffer::with_capacity(limit)
169        .map_err(|_| TransportError::ResponseBodyAllocationFailed)?;
170    loop {
171        let chunk = response
172            .chunk()
173            .await
174            .map_err(|_| TransportError::ResponseReadFailed)?;
175        let Some(chunk) = chunk else { break };
176        buffered
177            .extend_bounded(&chunk, limit)
178            .map_err(|_| TransportError::ResponseTooLarge)?;
179    }
180    Ok(buffered)
181}
182
183const fn map_method(method: Method) -> reqwest::Method {
184    match method {
185        Method::Get => reqwest::Method::GET,
186        Method::Post => reqwest::Method::POST,
187        Method::Put => reqwest::Method::PUT,
188        Method::Delete => reqwest::Method::DELETE,
189    }
190}
191
192fn classify_reqwest_error(error: reqwest::Error) -> TransportError {
193    if error.is_timeout() {
194        TransportError::TimedOut
195    } else if error.is_connect() {
196        TransportError::ConnectFailed
197    } else {
198        TransportError::RequestFailed
199    }
200}