use anyhow::{Result, anyhow};
use reqwest::{Client, Method, Response, StatusCode, Url};
use serde_json::Value;
const MAX_HOPS: u8 = 5;
pub const DEFAULT_GEN_HOST: &str = "https://api.rsclaw.ai";
pub fn gen_host_base(configured: Option<&str>) -> String {
let raw = configured
.filter(|s| !s.is_empty())
.unwrap_or(DEFAULT_GEN_HOST)
.trim_end_matches('/');
if let Some(s) = raw
.strip_suffix("/v1/agent")
.or_else(|| raw.strip_suffix("/v1"))
{
s.trim_end_matches('/').to_owned()
} else {
raw.to_owned()
}
}
pub fn build_client(user_agent: &str, timeout_secs: u64) -> Result<Client> {
Client::builder()
.user_agent(user_agent)
.timeout(std::time::Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|e| anyhow!("rsclaw_http: client build: {e}"))
}
pub async fn post_json(
client: &Client,
url: &str,
bearer: &str,
body: &Value,
) -> Result<Response> {
send_following(client, Method::POST, url, bearer, Some(body)).await
}
pub async fn get(client: &Client, url: &str, bearer: &str) -> Result<Response> {
send_following(client, Method::GET, url, bearer, None).await
}
pub async fn get_content_url(client: &Client, url: &str, bearer: &str) -> Result<Option<String>> {
let resp = client
.get(url)
.bearer_auth(bearer)
.send()
.await
.map_err(|e| anyhow!("rsclaw_http: GET {url}: {e}"))?;
let st = resp.status();
if st == StatusCode::TEMPORARY_REDIRECT || st == StatusCode::PERMANENT_REDIRECT {
let loc = resp
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| anyhow!("rsclaw_http: {st} omitted Location"))?;
return Ok(Some(resolve_location(url, loc)?));
}
if !st.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(anyhow!(
"rsclaw_http: GET {url} returned {st}: {}",
body.chars().take(200).collect::<String>()
));
}
Ok(None)
}
async fn send_following(
client: &Client,
method: Method,
initial_url: &str,
bearer: &str,
body: Option<&Value>,
) -> Result<Response> {
let mut current = initial_url.to_owned();
for _ in 0..=MAX_HOPS {
let mut builder = client
.request(method.clone(), ¤t)
.bearer_auth(bearer);
if let Some(b) = body {
builder = builder.json(b);
}
let resp = builder
.send()
.await
.map_err(|e| anyhow!("rsclaw_http: {method} {current}: {e}"))?;
let st = resp.status();
if st == StatusCode::TEMPORARY_REDIRECT || st == StatusCode::PERMANENT_REDIRECT {
let loc = resp
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| anyhow!("rsclaw_http: {st} omitted Location"))?
.to_owned();
current = resolve_location(¤t, &loc)?;
continue;
}
return Ok(resp);
}
Err(anyhow!(
"rsclaw_http: too many redirects starting from {initial_url}"
))
}
fn resolve_location(current: &str, loc: &str) -> Result<String> {
if loc.starts_with("http://") || loc.starts_with("https://") {
Ok(loc.to_owned())
} else {
Url::parse(current)
.and_then(|u| u.join(loc))
.map(|u| u.to_string())
.map_err(|e| anyhow!("rsclaw_http: resolve Location {loc:?}: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gen_host_base_strips_v1_agent() {
assert_eq!(
gen_host_base(Some("https://api.rsclaw.ai/v1/agent")),
"https://api.rsclaw.ai"
);
assert_eq!(
gen_host_base(Some("https://api.rsclaw.ai/v1/agent/")),
"https://api.rsclaw.ai"
);
}
#[test]
fn gen_host_base_strips_bare_v1() {
assert_eq!(
gen_host_base(Some("https://api.rsclaw.ai/v1")),
"https://api.rsclaw.ai"
);
}
#[test]
fn gen_host_base_preserves_already_root() {
assert_eq!(
gen_host_base(Some("https://api.rsclaw.ai")),
"https://api.rsclaw.ai"
);
assert_eq!(
gen_host_base(Some("https://api.rsclaw.ai/")),
"https://api.rsclaw.ai"
);
}
#[test]
fn gen_host_base_empty_or_none_yields_default() {
assert_eq!(gen_host_base(None), DEFAULT_GEN_HOST);
assert_eq!(gen_host_base(Some("")), DEFAULT_GEN_HOST);
}
#[test]
fn gen_host_base_custom_host_self_hosted() {
assert_eq!(
gen_host_base(Some("https://gen.internal:8443/v1/agent")),
"https://gen.internal:8443"
);
}
#[test]
fn resolve_location_absolute() {
assert_eq!(
resolve_location("https://api.rsclaw.ai/v1/videos", "https://backend-a.rsclaw.ai/v1/videos").unwrap(),
"https://backend-a.rsclaw.ai/v1/videos"
);
}
#[test]
fn resolve_location_relative_path() {
assert_eq!(
resolve_location("https://api.rsclaw.ai/v1/videos/video_abc", "/backend/v1/videos/video_abc").unwrap(),
"https://api.rsclaw.ai/backend/v1/videos/video_abc"
);
}
}