pakx_core/http_client.rs
1//! Shared `reqwest::Client` factory with project-wide timeouts.
2//!
3//! Every HTTP call in pakx ultimately funnels through `reqwest::Client`.
4//! Constructing one via `Client::new()` is convenient but leaves both
5//! the request timeout and the connect timeout at reqwest's default of
6//! **none** — so a half-open TCP connection to a slow / blackholed
7//! registry will hang the CLI indefinitely. The user can't even
8//! `Ctrl+C` out of the install loop cleanly because the futures are
9//! parked on the network, not on a tokio timer.
10//!
11//! This module centralises client construction so:
12//!
13//! - `request_timeout` defaults to 60s — long enough for an ordinary
14//! registry round-trip (federated search + metadata fetch) but short
15//! enough that a hung CI doesn't sit for 30 minutes before the job
16//! scheduler kills it.
17//! - `connect_timeout` defaults to 15s — fail fast on DNS / TCP issues
18//! instead of letting the request-timeout absorb the connect cost.
19//! - Long-running uploads (`pakx publish`, in particular the tarball
20//! PUT) can opt into a longer request timeout via
21//! [`http_client_with_timeout`].
22//!
23//! All other call sites that previously used `reqwest::Client::new()`
24//! must route through [`http_client`] so the timeout discipline is
25//! uniform across `install`, `publish`, `search`, `outdated`, `audit`,
26//! `add`, `info`, `upgrade`, and the registry sources. Auditing for
27//! drift is then a single `grep Client::new` across the workspace.
28
29use std::time::Duration;
30
31use reqwest::Client;
32
33/// Default request-level timeout applied to every client built via
34/// [`http_client`].
35///
36/// Long enough for the slowest legitimate registry round-trip we've
37/// seen in the wild (federated `pakx search` against a cold CDN),
38/// short enough that a wedged CI never sits indefinitely.
39pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
40
41/// Default TCP/TLS connect timeout. Separate from the request timeout
42/// so DNS / unreachable-host errors surface in seconds rather than
43/// being absorbed into the full 60s request budget.
44pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
45
46/// Request timeout for tarball uploads (`pakx publish` PUT). The
47/// default 60s is too aggressive for a 50 MiB upload over a slow
48/// residential uplink; 5 minutes matches the registry's own server-
49/// side limit.
50pub const UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
51
52/// Build a `reqwest::Client` with the project-wide default timeouts.
53///
54/// Panics only if `reqwest` itself fails to construct the underlying
55/// TLS stack — a programmer error (missing feature flag), not a
56/// runtime condition. Every call site in pakx already treats client
57/// construction as infallible, so panicking matches the prior
58/// `Client::new()` semantics exactly.
59#[must_use]
60pub fn http_client() -> Client {
61 http_client_with_timeout(DEFAULT_REQUEST_TIMEOUT)
62}
63
64/// Build a `reqwest::Client` with a caller-supplied request timeout
65/// and the project-wide default connect timeout.
66///
67/// Use this for code paths that exceed the default 60s budget —
68/// primarily tarball uploads in `pakx publish`.
69#[must_use]
70pub fn http_client_with_timeout(request_timeout: Duration) -> Client {
71 Client::builder()
72 .timeout(request_timeout)
73 .connect_timeout(DEFAULT_CONNECT_TIMEOUT)
74 .build()
75 .expect("http client builder")
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn http_client_builds() {
84 let _c = http_client();
85 }
86
87 #[test]
88 fn http_client_with_timeout_builds() {
89 let _c = http_client_with_timeout(Duration::from_secs(1));
90 let _c = http_client_with_timeout(UPLOAD_REQUEST_TIMEOUT);
91 }
92}