cloud_sdk_reqwest/asynchronous/
client.rs1use 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#[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 pub fn rotate_bearer_token(
45 &self,
46 replacement: BearerToken,
47 ) -> Result<(), CredentialStateError> {
48 self.credentials.rotate(replacement)
49 }
50
51 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 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}