1use std::sync::Arc;
10use std::time::Duration;
11
12use reqwest::Client as ReqwestClient;
13use reqwest::header::{ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
14use tokio::sync::Semaphore;
15use tracing::{debug, info, warn};
16use url::Url;
17
18use crate::core::auth::GoogleAuth;
19use crate::core::error::{GrrError, Result, api_error};
20
21pub(crate) const MAX_INFLIGHT_REQUESTS: usize = 64;
26const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
28const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
30const POOL_IDLE_PER_HOST: usize = 32;
32const POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
33const TCP_KEEPALIVE: Duration = Duration::from_secs(60);
34const H2_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
35const RETRY_ATTEMPTS: u32 = 3;
37const RETRY_BACKOFF_MS: u64 = 100;
39const MAX_RATE_LIMIT_WAIT: Duration = Duration::from_secs(30);
41
42pub(crate) fn parse_url(raw: &str, kind: &str) -> Result<Url> {
43 Url::parse(raw).map_err(|error| GrrError::Config(format!("Invalid {kind} URL: {error}")))
44}
45
46#[derive(Debug, PartialEq, Eq, Clone, Copy)]
47pub enum TransportMode {
48 Http3PriorKnowledge,
49 Http2PriorKnowledge,
50}
51
52pub fn resolve_transport_mode(use_http3: bool) -> TransportMode {
53 if use_http3 {
54 TransportMode::Http3PriorKnowledge
55 } else {
56 TransportMode::Http2PriorKnowledge
57 }
58}
59
60#[derive(Debug, Clone)]
61pub struct TransportInfo {
62 pub negotiated_version: String,
63 pub http3_requested: bool,
64 pub http3_effective: bool,
65 pub fell_back: bool,
66}
67
68impl Default for TransportInfo {
69 fn default() -> Self {
70 Self {
71 negotiated_version: "not-probed".into(),
72 http3_requested: true,
73 http3_effective: false,
74 fell_back: false,
75 }
76 }
77}
78
79#[derive(Clone)]
83pub struct HttpCore {
84 auth: Arc<GoogleAuth>,
85 http_client: ReqwestClient,
86 semaphore: Arc<Semaphore>,
87 transport_info: TransportInfo,
88}
89
90impl HttpCore {
91 pub async fn connect(auth: GoogleAuth, base_url: &Url, probe_path: &str) -> Result<Self> {
96 let mut info = TransportInfo {
97 http3_requested: true,
98 http3_effective: true,
99 ..TransportInfo::default()
100 };
101 let mut http_client = build_http_client()?;
102
103 match probe(
104 &http_client,
105 base_url,
106 &auth,
107 reqwest::Version::HTTP_3,
108 probe_path,
109 )
110 .await
111 {
112 Ok(version) => info.negotiated_version = version,
113 Err(GrrError::Auth(error)) => return Err(GrrError::Auth(error)),
114 Err(error) => {
115 warn!("HTTP/3 probe failed ({error}); rebuilding with HTTP/2");
116 info.http3_effective = false;
117 info.fell_back = true;
118 http_client = build_http_client_with_mode(TransportMode::Http2PriorKnowledge)?;
119 match probe(
120 &http_client,
121 base_url,
122 &auth,
123 reqwest::Version::HTTP_2,
124 probe_path,
125 )
126 .await
127 {
128 Ok(version) => info.negotiated_version = version,
129 Err(error) => {
130 warn!("HTTP/2 probe failed ({error}); continuing unprobed");
131 info.negotiated_version = "not-probed".into();
132 }
133 }
134 }
135 }
136
137 info!(
138 "HttpCore connected (probe {}: {}): http3_effective={}, max_inflight_requests={}",
139 probe_path, info.negotiated_version, info.http3_effective, MAX_INFLIGHT_REQUESTS
140 );
141
142 Ok(Self {
143 auth: Arc::new(auth),
144 http_client,
145 semaphore: Arc::new(Semaphore::new(MAX_INFLIGHT_REQUESTS)),
146 transport_info: info,
147 })
148 }
149
150 pub fn get(&self, url: Url) -> reqwest::RequestBuilder {
152 self.http_client.get(url)
153 }
154
155 pub fn post(&self, url: Url) -> reqwest::RequestBuilder {
157 self.http_client.post(url)
158 }
159
160 pub fn put(&self, url: Url) -> reqwest::RequestBuilder {
162 self.http_client.put(url)
163 }
164
165 pub fn patch(&self, url: Url) -> reqwest::RequestBuilder {
167 self.http_client.patch(url)
168 }
169
170 pub fn delete(&self, url: Url) -> reqwest::RequestBuilder {
172 self.http_client.delete(url)
173 }
174
175 pub fn http_client(&self) -> &ReqwestClient {
177 &self.http_client
178 }
179
180 pub fn auth(&self) -> &GoogleAuth {
182 &self.auth
183 }
184
185 pub fn transport_info(&self) -> &TransportInfo {
187 &self.transport_info
188 }
189
190 pub async fn execute(&self, mut request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
195 let _permit = self
196 .semaphore
197 .acquire()
198 .await
199 .map_err(|_| GrrError::Internal("Semaphore closed".into()))?;
200
201 let token = self.auth.get_access_token().await?;
202 request = request.header(AUTHORIZATION, format!("Bearer {}", token));
203 request = apply_transport_version(request, self.transport_info.http3_effective);
204
205 let mut pending = Some(request);
206 let mut last_error = None;
207
208 for attempt in 0..=RETRY_ATTEMPTS {
209 let to_send = match pending.take() {
212 None => break,
213 Some(builder) => match builder.try_clone() {
214 Some(replayable) => {
215 pending = Some(builder);
216 replayable
217 }
218 None => builder,
219 },
220 };
221
222 let response = to_send.send().await;
223
224 match response {
225 Ok(resp) => {
226 let status = resp.status();
227
228 if status.is_success() {
229 return Ok(resp);
230 }
231
232 match status.as_u16() {
233 401 => {
234 return Err(GrrError::Auth(
240 "request rejected as unauthorized (401); \
241 your access token is expired or invalid — \
242 rerun `grr auth login`"
243 .into(),
244 ));
245 }
246 429 => {
247 let retry_after = resp
251 .headers()
252 .get("retry-after")
253 .and_then(|h| h.to_str().ok())
254 .and_then(|s| s.parse::<u64>().ok())
255 .unwrap_or(60);
256 let wait = Duration::from_secs(retry_after);
257 if attempt < RETRY_ATTEMPTS && wait <= MAX_RATE_LIMIT_WAIT {
258 warn!(
259 "rate limited; honoring Retry-After of {retry_after}s \
260 (attempt {}/{})",
261 attempt + 1,
262 RETRY_ATTEMPTS
263 );
264 tokio::time::sleep(wait).await;
265 continue;
266 }
267 return Err(GrrError::RateLimited {
268 retry_after_secs: retry_after,
269 });
270 }
271 403 => {
272 return Err(GrrError::PermissionDenied(
273 "Insufficient permissions for this API or scope \
274 (was the API enabled in Google Cloud, and does \
275 your login include its scope? rerun `grr auth login` \
276 to grant new scopes)"
277 .into(),
278 ));
279 }
280 404 => {
281 return Err(GrrError::NotFound("Resource not found".into()));
282 }
283 500..=599 => {
284 let body = resp.text().await.unwrap_or_default();
285 last_error = Some(api_error(status.as_u16(), &body));
286 }
287 _ => {
288 let error_text = resp.text().await.unwrap_or_default();
289 return Err(api_error(status.as_u16(), &error_text));
290 }
291 }
292 }
293 Err(e) => {
294 if e.is_timeout() || e.is_connect() || e.is_request() {
295 last_error = Some(GrrError::Http(e));
296 } else {
297 return Err(GrrError::Http(e));
298 }
299 }
300 }
301
302 if attempt < RETRY_ATTEMPTS {
303 let backoff = RETRY_BACKOFF_MS * (2_u64.pow(attempt));
304 debug!(
305 "Request failed, retrying in {:?} (attempt {}/{})",
306 backoff,
307 attempt + 1,
308 RETRY_ATTEMPTS
309 );
310 tokio::time::sleep(Duration::from_millis(backoff)).await;
311 }
312 }
313
314 Err(last_error.unwrap_or_else(|| GrrError::Internal("Max retries exceeded".into())))
315 }
316}
317
318pub fn apply_transport_version(
325 builder: reqwest::RequestBuilder,
326 http3_effective: bool,
327) -> reqwest::RequestBuilder {
328 if http3_effective {
329 builder.version(reqwest::Version::HTTP_3)
330 } else {
331 builder
332 }
333}
334
335async fn probe(
341 client: &ReqwestClient,
342 base: &Url,
343 auth: &GoogleAuth,
344 version: reqwest::Version,
345 probe_path: &str,
346) -> std::result::Result<String, GrrError> {
347 let token = auth.get_access_token().await?;
348 let url = base.join(probe_path)?;
349 let resp = client
350 .get(url)
351 .bearer_auth(&token)
352 .version(version)
353 .send()
354 .await?;
355 Ok(format!("{:?}", resp.version()))
356}
357
358pub fn build_http_client() -> Result<ReqwestClient> {
363 build_http_client_with_mode(resolve_transport_mode(true))
364}
365
366fn build_http_client_with_mode(mode: TransportMode) -> Result<ReqwestClient> {
367 let mut builder = ReqwestClient::builder()
368 .timeout(REQUEST_TIMEOUT)
369 .connect_timeout(CONNECT_TIMEOUT)
370 .pool_max_idle_per_host(POOL_IDLE_PER_HOST)
371 .pool_idle_timeout(POOL_IDLE_TIMEOUT)
372 .tcp_keepalive(TCP_KEEPALIVE)
373 .http2_keep_alive_interval(H2_KEEPALIVE_INTERVAL)
374 .http2_adaptive_window(true)
375 .brotli(true)
376 .zstd(true)
377 .gzip(true);
378
379 builder = match mode {
380 TransportMode::Http3PriorKnowledge => builder.http3_prior_knowledge(),
381 TransportMode::Http2PriorKnowledge => builder.http2_prior_knowledge(),
382 };
383
384 let mut headers = HeaderMap::new();
385 headers.insert(
386 ACCEPT_ENCODING,
387 HeaderValue::from_static("zstd, br, gzip, deflate"),
388 );
389 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
390
391 builder
392 .default_headers(headers)
393 .build()
394 .map_err(|error| GrrError::Config(format!("Failed to build HTTP client: {error}")))
395}