1use crate::tool::Tool;
4use async_trait::async_trait;
5use serde_json::json;
6use std::collections::HashMap;
7use std::time::Duration;
8
9const DEFAULT_TIMEOUT_SECS: u64 = 30;
10const MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024; pub struct HttpRequestTool;
14
15impl HttpRequestTool {
16 pub fn new() -> Self {
17 Self
18 }
19}
20
21impl Default for HttpRequestTool {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27#[async_trait]
28impl Tool for HttpRequestTool {
29 fn name(&self) -> &str {
30 "http_request"
31 }
32
33 fn description(&self) -> &str {
34 "Make HTTP requests (GET, POST, PUT, DELETE, etc.) to APIs and web services"
35 }
36
37 fn parameters_schema(&self) -> serde_json::Value {
38 json!({
39 "type": "object",
40 "properties": {
41 "url": {
42 "type": "string",
43 "description": "URL to request"
44 },
45 "method": {
46 "type": "string",
47 "enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
48 "default": "GET"
49 },
50 "headers": {
51 "type": "object",
52 "description": "HTTP headers as key-value pairs"
53 },
54 "body": {
55 "type": ["string", "object"],
56 "description": "Request body (for POST, PUT, PATCH)"
57 },
58 "timeout": {
59 "type": "integer",
60 "description": "Timeout in seconds (default: 30)",
61 "minimum": 1,
62 "maximum": 300
63 }
64 },
65 "required": ["url"]
66 })
67 }
68
69 fn requires_network(&self) -> bool {
70 true
71 }
72
73 async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
74 let url = args
75 .get("url")
76 .and_then(|v| v.as_str())
77 .ok_or_else(|| anyhow::anyhow!("Missing 'url' parameter"))?;
78
79 let method = args.get("method").and_then(|v| v.as_str()).unwrap_or("GET");
80
81 let timeout_secs = args
82 .get("timeout")
83 .and_then(|v| v.as_u64())
84 .unwrap_or(DEFAULT_TIMEOUT_SECS);
85
86 let client = reqwest::Client::builder()
87 .timeout(Duration::from_secs(timeout_secs))
88 .build()?;
89
90 let mut request = match method.to_uppercase().as_str() {
91 "GET" => client.get(url),
92 "POST" => client.post(url),
93 "PUT" => client.put(url),
94 "DELETE" => client.delete(url),
95 "PATCH" => client.patch(url),
96 "HEAD" => client.head(url),
97 _ => client.get(url),
98 };
99
100 if let Some(headers) = args.get("headers").and_then(|h| h.as_object()) {
102 for (key, value) in headers {
103 if let Some(val_str) = value.as_str() {
104 request = request.header(key, val_str);
105 }
106 }
107 }
108
109 if let Some(body) = args.get("body") {
111 let body_str = if body.is_object() {
112 serde_json::to_string(body)?
113 } else {
114 body.as_str().unwrap_or("").to_string()
115 };
116 request = request.body(body_str);
117 }
118
119 let response = request.send().await?;
120 let status = response.status();
121 let headers: HashMap<String, String> = response
122 .headers()
123 .iter()
124 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
125 .collect();
126
127 let content_type = response
128 .headers()
129 .get("content-type")
130 .and_then(|v| v.to_str().ok())
131 .unwrap_or("")
132 .to_lowercase();
133
134 let body_bytes = response.bytes().await?;
136 if body_bytes.len() > MAX_RESPONSE_SIZE {
137 anyhow::bail!(
138 "Response too large: {} bytes (max: {})",
139 body_bytes.len(),
140 MAX_RESPONSE_SIZE
141 );
142 }
143
144 let body = if content_type.contains("application/json") {
146 match serde_json::from_slice::<serde_json::Value>(&body_bytes) {
147 Ok(json) => json,
148 Err(_) => {
149 serde_json::Value::String(String::from_utf8_lossy(&body_bytes).to_string())
150 }
151 }
152 } else {
153 serde_json::Value::String(String::from_utf8_lossy(&body_bytes).to_string())
154 };
155
156 Ok(json!({
157 "success": status.is_success(),
158 "status_code": status.as_u16(),
159 "status_text": status.canonical_reason(),
160 "headers": headers,
161 "body": body,
162 "url": url,
163 "method": method
164 }))
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[tokio::test]
173 async fn test_http_get() {
174 let tool = HttpRequestTool::new();
175 let result = tool
176 .execute(json!({
177 "url": "https://httpbin.org/get",
178 "method": "GET"
179 }))
180 .await;
181
182 if let Ok(response) = result {
184 assert_eq!(response["success"], true);
185 assert_eq!(response["status_code"], 200);
186 }
187 }
188
189 #[tokio::test]
190 async fn test_http_post() {
191 let tool = HttpRequestTool::new();
192 let result = tool
193 .execute(json!({
194 "url": "https://httpbin.org/post",
195 "method": "POST",
196 "body": { "test": "data" }
197 }))
198 .await;
199
200 if let Ok(response) = result {
201 assert_eq!(response["success"], true);
202 assert_eq!(response["status_code"], 200);
203 }
204 }
205}