Skip to main content

codetether_browser/browser/offline/
record.rs

1//! Capture an HTTP GET (status + headers + body) to JSON. Body is base64
2//! so binary payloads survive a round-trip through replay.
3
4use anyhow::Result;
5use base64::Engine as _;
6use base64::engine::general_purpose::STANDARD;
7use reqwest::blocking::Client;
8use std::collections::BTreeMap;
9use std::path::Path;
10
11#[derive(Debug, serde::Serialize, serde::Deserialize)]
12pub struct Capture {
13    pub url: String,
14    pub status: u16,
15    pub headers: BTreeMap<String, String>,
16    pub content_type: Option<String>,
17    /// Response body, base64-encoded so binary payloads survive serialization.
18    pub body_base64: String,
19}
20
21pub fn run(url: &str, out: &Path) -> Result<String> {
22    let resp = Client::builder().build()?.get(url).send()?;
23    let status = resp.status().as_u16();
24    let headers: BTreeMap<String, String> = resp
25        .headers()
26        .iter()
27        .map(|(k, v)| {
28            (
29                k.as_str().to_ascii_lowercase(),
30                String::from_utf8_lossy(v.as_bytes()).to_string(),
31            )
32        })
33        .collect();
34    let content_type = headers.get("content-type").cloned();
35    let bytes = resp.bytes()?;
36    let capture = Capture {
37        url: url.into(),
38        status,
39        headers,
40        content_type,
41        body_base64: STANDARD.encode(&bytes),
42    };
43    std::fs::write(out, serde_json::to_string_pretty(&capture)?)?;
44    Ok(serde_json::to_string_pretty(&serde_json::json!({
45        "saved": out.display().to_string(),
46        "status": capture.status,
47        "bytes": bytes.len(),
48        "content_type": capture.content_type,
49    }))?)
50}