Skip to main content

agent_first_http/sdk/fetch/artifacts/
body.rs

1//! Raw HTTP response body artifact (`body.<ext>`).
2
3use std::path::PathBuf;
4
5use crate::sdk::fetch::writer;
6use crate::shared::artifacts::{Artifact, ArtifactPaths};
7use crate::shared::error::Error;
8
9/// Write `bytes` as the body artifact. `content_type` is consulted to pick
10/// the file extension; unknown types fall back to `.bin`.
11pub async fn write(
12    paths: &ArtifactPaths,
13    content_type: Option<&str>,
14    bytes: &[u8],
15) -> Result<PathBuf, Error> {
16    let ext = extension_for(content_type);
17    let mut target = paths.file_for(Artifact::Body);
18    target.set_extension(ext);
19    writer::write_bytes(&target, bytes).await?;
20    Ok(target)
21}
22
23fn extension_for(content_type: Option<&str>) -> &'static str {
24    let Some(ct) = content_type else { return "bin" };
25    let primary = ct
26        .split(';')
27        .next()
28        .unwrap_or(ct)
29        .trim()
30        .to_ascii_lowercase();
31    match primary.as_str() {
32        "text/html" | "application/xhtml+xml" => "html",
33        "application/json" | "application/ld+json" => "json",
34        "application/javascript" | "text/javascript" => "js",
35        "text/css" => "css",
36        "text/plain" => "txt",
37        "image/png" => "png",
38        "image/jpeg" | "image/jpg" => "jpg",
39        "image/gif" => "gif",
40        "image/webp" => "webp",
41        "image/svg+xml" => "svg",
42        "application/xml" | "text/xml" => "xml",
43        "application/pdf" => "pdf",
44        "application/octet-stream" => "bin",
45        _ => mime_guess::get_mime_extensions_str(&primary)
46            .and_then(|exts| exts.first())
47            .copied()
48            .unwrap_or("bin"),
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::shared::ids::RequestId;
56
57    #[test]
58    fn extension_table_covers_common_mimes() {
59        assert_eq!(extension_for(Some("text/html")), "html");
60        assert_eq!(extension_for(Some("text/html; charset=utf-8")), "html");
61        assert_eq!(extension_for(Some("application/json")), "json");
62        assert_eq!(extension_for(Some("image/png")), "png");
63        assert_eq!(extension_for(None), "bin");
64        assert_eq!(extension_for(Some("application/x-weird-thing")), "bin");
65    }
66
67    #[tokio::test]
68    async fn writes_body_with_html_extension() {
69        let dir = tempfile::tempdir().unwrap();
70        let rid = RequestId::new_v4();
71        let paths = ArtifactPaths::new(dir.path().to_path_buf(), &rid);
72        let p = write(&paths, Some("text/html"), b"<html></html>")
73            .await
74            .unwrap();
75        assert_eq!(p.extension().and_then(|s| s.to_str()), Some("html"));
76        let content = tokio::fs::read(&p).await.unwrap();
77        assert_eq!(content, b"<html></html>");
78    }
79}