Skip to main content

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}