xbp 10.14.2

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use serde_json::Value;
use std::time::Duration;
use tokio::time::{sleep, timeout};
use tracing::debug;

fn canonicalize_url(raw: &str) -> String {
    let raw = raw.trim();
    if raw.starts_with("https://https://") {
        raw["https://".len()..].to_string()
    } else if raw.starts_with("https://") || raw.starts_with("http://") {
        raw.to_string()
    } else {
        format!("https://{}", raw)
    }
}

pub async fn run_curl(raw_url: &str, no_timeout: bool, debug: bool) -> Result<(), String> {
    let url = canonicalize_url(raw_url);
    let client = reqwest::Client::builder()
        .redirect(reqwest::redirect::Policy::limited(10))
        .build()
        .map_err(|e| format!("failed to build HTTP client: {}", e))?;

    if debug {
        debug!("Requesting {}", url);
    }

    let progress = tokio::spawn(async {
        sleep(Duration::from_secs(5)).await;
        eprintln!("Taking longer than expected to respond...");
    });

    let request = async {
        let response = client
            .get(&url)
            .send()
            .await
            .map_err(|e| format!("request failed: {}", e))?;

        let status = response.status();
        let headers = response.headers().clone();
        let body = response
            .text()
            .await
            .map_err(|e| format!("failed to read response body: {}", e))?;

        Ok::<_, String>((status, headers, body))
    };

    let result = if no_timeout {
        request.await
    } else {
        timeout(Duration::from_secs(15), request)
            .await
            .map_err(|_| {
                format!(
                    "Request to {} timed out after 15 seconds. Use --no-timeout to wait longer.",
                    url
                )
            })?
    };

    progress.abort();

    let (status, headers, body) = result?;

    println!("=== Curl to {}", url);
    println!("--- Status --- {}", status);
    println!("--- Headers ---");
    for (name, value) in &headers {
        println!("{}: {}", name, value.to_str().unwrap_or("<binary>"));
    }
    println!("--- Body ---");

    if let Ok(json) = serde_json::from_str::<Value>(&body) {
        let pretty = serde_json::to_string_pretty(&json)
            .map_err(|e| format!("failed to pretty-print JSON body: {}", e))?;
        println!("{}", pretty);
    } else if body.is_empty() {
        println!("<empty>");
    } else {
        println!("{}", body);
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::canonicalize_url;

    #[test]
    fn canonicalizes_plain_domains() {
        assert_eq!(canonicalize_url("example.com"), "https://example.com");
    }

    #[test]
    fn deduplicates_https_prefix() {
        assert_eq!(
            canonicalize_url("https://https://example.com"),
            "https://example.com"
        );
    }
}