use crate::tool::Tool;
use async_trait::async_trait;
use serde_json::json;
use std::collections::HashMap;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 30;
const MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
pub struct HttpRequestTool;
impl HttpRequestTool {
pub fn new() -> Self {
Self
}
}
impl Default for HttpRequestTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for HttpRequestTool {
fn name(&self) -> &str {
"http_request"
}
fn description(&self) -> &str {
"Make HTTP requests (GET, POST, PUT, DELETE, etc.) to APIs and web services"
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to request"
},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
"default": "GET"
},
"headers": {
"type": "object",
"description": "HTTP headers as key-value pairs"
},
"body": {
"type": ["string", "object"],
"description": "Request body (for POST, PUT, PATCH)"
},
"timeout": {
"type": "integer",
"description": "Timeout in seconds (default: 30)",
"minimum": 1,
"maximum": 300
}
},
"required": ["url"]
})
}
fn requires_network(&self) -> bool {
true
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
let url = args
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'url' parameter"))?;
let method = args.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
let timeout_secs = args
.get("timeout")
.and_then(|v| v.as_u64())
.unwrap_or(DEFAULT_TIMEOUT_SECS);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()?;
let mut request = match method.to_uppercase().as_str() {
"GET" => client.get(url),
"POST" => client.post(url),
"PUT" => client.put(url),
"DELETE" => client.delete(url),
"PATCH" => client.patch(url),
"HEAD" => client.head(url),
_ => client.get(url),
};
if let Some(headers) = args.get("headers").and_then(|h| h.as_object()) {
for (key, value) in headers {
if let Some(val_str) = value.as_str() {
request = request.header(key, val_str);
}
}
}
if let Some(body) = args.get("body") {
let body_str = if body.is_object() {
serde_json::to_string(body)?
} else {
body.as_str().unwrap_or("").to_string()
};
request = request.body(body_str);
}
let response = request.send().await?;
let status = response.status();
let headers: HashMap<String, String> = response
.headers()
.iter()
.filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
.collect();
let content_type = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_lowercase();
let body_bytes = response.bytes().await?;
if body_bytes.len() > MAX_RESPONSE_SIZE {
anyhow::bail!(
"Response too large: {} bytes (max: {})",
body_bytes.len(),
MAX_RESPONSE_SIZE
);
}
let body = if content_type.contains("application/json") {
match serde_json::from_slice::<serde_json::Value>(&body_bytes) {
Ok(json) => json,
Err(_) => {
serde_json::Value::String(String::from_utf8_lossy(&body_bytes).to_string())
}
}
} else {
serde_json::Value::String(String::from_utf8_lossy(&body_bytes).to_string())
};
Ok(json!({
"success": status.is_success(),
"status_code": status.as_u16(),
"status_text": status.canonical_reason(),
"headers": headers,
"body": body,
"url": url,
"method": method
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_http_get() {
let tool = HttpRequestTool::new();
let result = tool
.execute(json!({
"url": "https://httpbin.org/get",
"method": "GET"
}))
.await;
if let Ok(response) = result {
assert_eq!(response["success"], true);
assert_eq!(response["status_code"], 200);
}
}
#[tokio::test]
async fn test_http_post() {
let tool = HttpRequestTool::new();
let result = tool
.execute(json!({
"url": "https://httpbin.org/post",
"method": "POST",
"body": { "test": "data" }
}))
.await;
if let Ok(response) = result {
assert_eq!(response["success"], true);
assert_eq!(response["status_code"], 200);
}
}
}