Skip to main content

enact_core/tool/
http.rs

1//! HTTP request tool
2
3use 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; // 10MB
11
12/// HTTP request tool for making API calls
13pub 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        // Add headers
101        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        // Add body
110        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        // Read body with size limit
135        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        // Try to parse as JSON, fallback to text
145        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        // May fail if no network
183        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}