cloud_sdk_reqwest/asynchronous/
client.rs1use core::fmt;
2
3use cloud_sdk::Method;
4use cloud_sdk::transport::{AsyncTransport, StatusCode, TransportRequest, TransportResponse};
5use cloud_sdk_sanitization::sanitize_bytes;
6use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderValue};
7use reqwest::{Body, Client};
8
9use crate::shared::{BearerToken, HttpsEndpoint, TransportError, parse_rate_limit};
10
11use super::body::SanitizedBuffer;
12
13pub struct AsyncClient {
18 client: Client,
19 endpoint: HttpsEndpoint,
20 token: BearerToken,
21}
22
23impl AsyncClient {
24 pub(super) const fn new(client: Client, endpoint: HttpsEndpoint, token: BearerToken) -> Self {
25 Self {
26 client,
27 endpoint,
28 token,
29 }
30 }
31
32 async fn send_inner<'buffer>(
33 &mut self,
34 request: TransportRequest<'_>,
35 response_body: &'buffer mut [u8],
36 ) -> Result<TransportResponse<'buffer>, TransportError> {
37 sanitize_bytes(response_body);
38 let url = self
39 .endpoint
40 .compose(request.target())
41 .map_err(|_| TransportError::TargetRejected)?;
42 let authorization = self
43 .token
44 .header_value()
45 .map_err(|_| TransportError::HeaderRejected)?;
46 let mut outbound = self
47 .client
48 .request(map_method(request.method()), url)
49 .header(AUTHORIZATION, authorization)
50 .header(ACCEPT, HeaderValue::from_static("application/json"));
51
52 if let Some(content_type) = request.content_type() {
53 let value = HeaderValue::from_str(content_type.as_str())
54 .map_err(|_| TransportError::HeaderRejected)?;
55 outbound = outbound.header(CONTENT_TYPE, value);
56 } else if !request.body().is_empty() {
57 return Err(TransportError::MissingContentType);
58 }
59
60 if !request.body().is_empty() {
61 let body = SanitizedBuffer::copy_from(request.body())
62 .map_err(|_| TransportError::RequestBodyAllocationFailed)?;
63 let _ = u64::try_from(request.body().len())
64 .map_err(|_| TransportError::RequestBodyTooLarge)?;
65 outbound = outbound.body(Body::from(body.into_bytes()));
66 }
67
68 let mut response = outbound.send().await.map_err(classify_reqwest_error)?;
69 self.endpoint
70 .verify_origin(response.url())
71 .map_err(|_| TransportError::ResponseOriginChanged)?;
72 if response.content_length().is_some_and(|length| {
73 u64::try_from(response_body.len()).map_or(true, |cap| length > cap)
74 }) {
75 return Err(TransportError::ResponseTooLarge);
76 }
77 let status =
78 StatusCode::new(response.status().as_u16()).ok_or(TransportError::InvalidStatus)?;
79 let rate_limit = parse_rate_limit(response.headers())?;
80 let buffered = read_response(&mut response, response_body.len()).await?;
81 let body_len = buffered.len();
82 let initialized = response_body
83 .get_mut(..body_len)
84 .ok_or(TransportError::ResponseReadFailed)?;
85 initialized.copy_from_slice(buffered.as_ref());
86 let response = TransportResponse::new(status, initialized);
87 Ok(rate_limit.map_or(response, |value| response.with_rate_limit(value)))
88 }
89}
90
91impl AsyncTransport for AsyncClient {
92 type Error = TransportError;
93
94 async fn send<'transport, 'request, 'buffer>(
95 &'transport mut self,
96 request: TransportRequest<'request>,
97 response_body: &'buffer mut [u8],
98 ) -> Result<TransportResponse<'buffer>, Self::Error>
99 where
100 'request: 'transport,
101 'buffer: 'transport,
102 {
103 self.send_inner(request, response_body).await
104 }
105}
106
107impl fmt::Debug for AsyncClient {
108 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109 formatter
110 .debug_struct("AsyncClient")
111 .field("endpoint", &"[redacted]")
112 .field("token", &"[redacted]")
113 .finish_non_exhaustive()
114 }
115}
116
117async fn read_response(
118 response: &mut reqwest::Response,
119 limit: usize,
120) -> Result<SanitizedBuffer, TransportError> {
121 let mut buffered = SanitizedBuffer::with_capacity(limit)
122 .map_err(|_| TransportError::ResponseBodyAllocationFailed)?;
123 loop {
124 let chunk = response
125 .chunk()
126 .await
127 .map_err(|_| TransportError::ResponseReadFailed)?;
128 let Some(chunk) = chunk else { break };
129 buffered
130 .extend_bounded(&chunk, limit)
131 .map_err(|_| TransportError::ResponseTooLarge)?;
132 }
133 Ok(buffered)
134}
135
136const fn map_method(method: Method) -> reqwest::Method {
137 match method {
138 Method::Get => reqwest::Method::GET,
139 Method::Post => reqwest::Method::POST,
140 Method::Put => reqwest::Method::PUT,
141 Method::Delete => reqwest::Method::DELETE,
142 }
143}
144
145fn classify_reqwest_error(error: reqwest::Error) -> TransportError {
146 if error.is_timeout() {
147 TransportError::TimedOut
148 } else if error.is_connect() {
149 TransportError::ConnectFailed
150 } else {
151 TransportError::RequestFailed
152 }
153}