use std::time::Instant;
use tokio::process::Command;
use tracing::{debug, info, warn};
fn canonicalize_url(raw: &str) -> String {
let raw = raw.trim();
if raw.starts_with("https://https://") {
let dedup = &raw["https://".len()..];
format!("https://{}", dedup)
} 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, debug: bool) -> Result<(), String> {
let url = canonicalize_url(raw_url);
let mut curl_cmd = Command::new("curl");
curl_cmd
.arg("-sS") .arg("-D")
.arg("-") .arg("-w")
.arg("%{http_code}") .arg(&url);
if debug {
debug!("running curl command for '{}'", url);
}
let start = Instant::now();
let output = curl_cmd
.output()
.await
.map_err(|e| format!("failed to run curl: {}", e))?;
let duration = start.elapsed();
if !output.status.success() {
return Err(format!(
"\x1b[91mCurl failed with status: {}\nStderr: {}\x1b[0m",
output.status,
String::from_utf8_lossy(&output.stderr)
));
}
let all_output = output.stdout;
let all_str = String::from_utf8_lossy(&all_output);
let mut lines = all_str.lines();
let mut headers = Vec::new();
let mut body = String::new();
let mut in_headers = true;
for line in lines.by_ref() {
if in_headers && line.trim() == "" {
in_headers = false;
continue;
}
if in_headers {
headers.push(line.to_string());
} else {
body.push_str(line);
body.push('\n');
}
}
let mut status_code = None;
if !body.is_empty() && body.trim().chars().all(|c| c.is_ascii_digit()) {
status_code = Some(body.trim().to_string());
body.clear();
} else if let Some(code_line) = lines.last() {
if code_line.chars().all(|c| c.is_ascii_digit()) {
status_code = Some(code_line.to_string());
}
}
info!("=== Curl to {}", url);
info!("--- Response Headers ---");
for h in &headers {
info!("{}", h);
}
if let Some(status) = &status_code {
info!("--- Status --- {}", status);
}
info!("--- Response Time --- {:.2?}", duration);
let mut jq_cmd = Command::new("jq");
jq_cmd.arg("-C").arg(".");
if debug {
debug!("piping response body to jq for colored pretty-print");
}
let mut jq_child = jq_cmd
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("failed to spawn jq: {}", e))?;
if let Some(mut stdin) = jq_child.stdin.take() {
use tokio::io::AsyncWriteExt;
stdin
.write_all(body.as_bytes())
.await
.map_err(|e| format!("failed to write to jq stdin: {}", e))?;
}
let jq_output = jq_child
.wait_with_output()
.await
.map_err(|e| format!("failed to collect jq output: {}", e))?;
info!("--- Body (pretty JSON) ---");
info!("{}", String::from_utf8_lossy(&jq_output.stdout));
if !jq_output.status.success() {
warn!("jq encountered an error (body may not be JSON)");
if !body.trim().is_empty() {
info!("--- Raw Body ---");
info!("{}", body);
}
}
Ok(())
}