headless_lms_utils/http.rs
1use std::time::Duration;
2
3use headless_lms_base::config::bool_env_false_by_default;
4use once_cell::sync::Lazy;
5
6/// Total deadline for a single request/response exchange, for callers that do not set their own.
7/// Never apply it to a streaming request: reqwest counts it until the response body has finished,
8/// so it would cut off a long but healthy stream.
9pub const NON_STREAMING_REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
10
11/// How long a streaming request may take to produce response headers. Applied by the caller with
12/// `tokio::time::timeout` around `send()`, which resolves on headers, so the body is left untimed.
13pub const STREAM_RESPONSE_HEADERS_TIMEOUT: Duration = Duration::from_secs(120);
14
15/// How long a streaming response body may go without producing a chunk before it counts as stalled.
16/// Applied per chunk by the caller, unlike `ClientBuilder::read_timeout`; see `REQWEST_STREAMING_CLIENT`.
17pub const STREAM_IDLE_TIMEOUT: Duration = Duration::from_secs(120);
18
19/// How long establishing the connection may take, before anything is sent.
20pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
21
22// Reads env vars directly instead of caching in ApplicationConfiguration, which isn't static.
23fn in_http_test_mode() -> bool {
24 bool_env_false_by_default("USE_MOCK_AZURE_CONFIGURATION")
25 || bool_env_false_by_default("TEST_MODE")
26}
27
28/// The settings every HTTP client shares: rustls, https only outside test mode, and the connect
29/// timeout. Other timeouts are left to the caller.
30pub fn base_client_builder() -> reqwest::ClientBuilder {
31 reqwest::Client::builder()
32 .use_rustls_tls()
33 .https_only(!in_http_test_mode())
34 .connect_timeout(CONNECT_TIMEOUT)
35}
36
37/// Escapes a value for a quoted-string auth-param of a `WWW-Authenticate` header (RFC 9110 ยง11.2).
38pub fn escape_auth_param(value: &str) -> String {
39 value.replace('\\', "\\\\").replace('"', "\\\"")
40}
41
42pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
43 if in_http_test_mode() {
44 warn!("Test environment. REQWEST_CLIENT is allowed to make http requests");
45 }
46
47 base_client_builder()
48 // Default deadline only: a per-request `.timeout()` takes precedence over it. Deliberately
49 // not `read_timeout`, whose sleep is armed once when the request is created and polled
50 // before the response future, making it a flat ceiling on the wait for headers that no
51 // per-request timeout can raise.
52 .timeout(NON_STREAMING_REQUEST_TIMEOUT)
53 .build()
54 .expect("Failed to build Client")
55});
56
57/// For responses whose body is consumed as a stream. Carries no total or read timeout, because both
58/// would cut off a healthy but slow stream; callers must instead bound the header wait with
59/// [`STREAM_RESPONSE_HEADERS_TIMEOUT`] and each chunk with [`STREAM_IDLE_TIMEOUT`].
60pub static REQWEST_STREAMING_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
61 base_client_builder()
62 .build()
63 .expect("Failed to build streaming Client")
64});