Skip to main content

agentsec_core/web/
fetch.rs

1//! HTTP fetch with a hard body cap and a fixed timeout.
2//!
3//! Constants:
4//!
5//! - `DEFAULT_TIMEOUT_SECS` — request timeout (currently 15 s).
6//! - `MAX_BODY_BYTES` — hard cap on response size (currently 2 MiB).
7//! - `USER_AGENT` — sent as `User-Agent` header; format is
8//!   `agentsec/<cargo-pkg-version>`.
9//!
10//! Non-2xx responses and oversized bodies are rejected via
11//! [`crate::Error::Sanitize`]. The body is **not** truncated; the request
12//! is failed.
13
14use crate::error::{Error, Result};
15use std::time::Duration;
16
17const DEFAULT_TIMEOUT_SECS: u64 = 15;
18const MAX_BODY_BYTES: usize = 2 * 1024 * 1024;
19const USER_AGENT: &str = concat!("agentsec/", env!("CARGO_PKG_VERSION"));
20
21/// Fetch a URL with a 2 MiB body cap and a 15 s timeout.
22///
23/// Returns the raw body decoded as UTF-8 (lossy) on success.
24///
25/// # Errors
26///
27/// - [`crate::Error::Http`] — reqwest client / connect / read error.
28/// - [`crate::Error::Sanitize`] — HTTP non-2xx status, or body size strictly
29///   greater than 2 MiB.
30///
31/// The caller (sanitize pipeline) is responsible for treating the returned
32/// content as untrusted.
33pub async fn get(url: &str) -> Result<String> {
34    let client = reqwest::Client::builder()
35        .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
36        .user_agent(USER_AGENT)
37        .build()?;
38
39    let resp = client.get(url).send().await?;
40    let status = resp.status();
41    if !status.is_success() {
42        return Err(Error::Sanitize(format!(
43            "fetch {url} returned HTTP {status}"
44        )));
45    }
46
47    // Read body with a hard cap. reqwest::Response::text() has no size limit,
48    // so we stream bytes and stop when we exceed MAX_BODY_BYTES.
49    let bytes = resp.bytes().await?;
50    if bytes.len() > MAX_BODY_BYTES {
51        return Err(Error::Sanitize(format!(
52            "fetch {url} body exceeds {MAX_BODY_BYTES} byte cap"
53        )));
54    }
55    let text = String::from_utf8_lossy(&bytes).into_owned();
56    Ok(text)
57}