65 lines
2.5 KiB
Rust
65 lines
2.5 KiB
Rust
//! The one outbound HTTP client, bounded. Every server-side fetch -
|
|||
|
|
//! Gitea content at boot and on reload, the raw-asset proxy, the
|
||
|
|
//! repo-info endpoint, and the content-declared `url` resource - goes
|
||
|
|
//! through here, so a slow or oversized upstream can cost a request at
|
||
|
|
//! most `TIMEOUT` and `cap` bytes, never a thread or the heap. Before
|
||
|
|
//! this module each call site built its own client with no timeout and
|
||
|
|
//! read whole bodies with `.text()`.
|
||
|
|
#![cfg(feature = "ssr")]
|
||
|
|
|
||
|
|
use std::sync::OnceLock;
|
||
|
|
use std::time::Duration;
|
||
|
|
|
||
|
|
use openidconnect::reqwest;
|
||
|
|
|
||
|
|
/// Whole-request budget: connect, headers, and body.
|
||
|
|
pub const TIMEOUT: Duration = Duration::from_secs(20);
|
||
|
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||
|
|
|
||
|
|
/// A content-declared URL resource: JSON for one card.
|
||
|
|
pub const URL_RESOURCE_CAP: usize = 2 * 1024 * 1024;
|
||
|
|
/// A Gitea API reply or one content file (a tree listing, a YAML page).
|
||
|
|
pub const GITEA_CAP: usize = 8 * 1024 * 1024;
|
||
|
|
/// A content-shipped asset proxied to the browser (an image, a font).
|
||
|
|
pub const ASSET_CAP: usize = 32 * 1024 * 1024;
|
||
|
|
|
||
|
|
pub fn client() -> &'static reqwest::Client {
|
||
|
|
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
|
||
|
|
CLIENT.get_or_init(|| {
|
||
|
|
reqwest::Client::builder()
|
||
|
|
.connect_timeout(CONNECT_TIMEOUT)
|
||
|
|
.timeout(TIMEOUT)
|
||
|
|
.redirect(reqwest::redirect::Policy::limited(5))
|
||
|
|
.user_agent(concat!("portal/", env!("CARGO_PKG_VERSION")))
|
||
|
|
.build()
|
||
|
|
.expect("a client with static options always builds")
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Reads a body up to `cap` bytes; one byte more is an error, not a
|
||
|
|
/// truncation, so a caller never parses half a document.
|
||
|
|
pub async fn bytes(mut response: reqwest::Response, cap: usize) -> anyhow::Result<Vec<u8>> {
|
||
|
|
let hint = response.content_length().unwrap_or(0) as usize;
|
||
|
|
if hint > cap {
|
||
|
|
anyhow::bail!("response of {hint} bytes exceeds the cap of {cap}");
|
||
|
|
}
|
||
|
|
let mut out = Vec::with_capacity(hint.min(cap));
|
||
|
|
while let Some(chunk) = response.chunk().await? {
|
||
|
|
if out.len() + chunk.len() > cap {
|
||
|
|
anyhow::bail!("response exceeds the cap of {cap} bytes");
|
||
|
|
}
|
||
|
|
out.extend_from_slice(&chunk);
|
||
|
|
}
|
||
|
|
Ok(out)
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn text(response: reqwest::Response, cap: usize) -> anyhow::Result<String> {
|
||
|
|
Ok(String::from_utf8(bytes(response, cap).await?)?)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// `GET url`, status checked, body read under `cap`.
|
||
|
|
pub async fn get_text(url: &str, cap: usize) -> anyhow::Result<String> {
|
||
|
|
let response = client().get(url).send().await?.error_for_status()?;
|
||
|
|
text(response, cap).await
|
||
|
|
}
|