1use std::time::Duration;
28
29use crate::agent::StepError;
30use ureq::{self, Agent, RequestBuilder};
31
32pub fn http_get(url: &str) -> Result<String, StepError> {
34 http_get_with_headers(url, &[])
35}
36
37pub fn http_get_with_headers(url: &str, headers: &[(&str, &str)]) -> Result<String, StepError> {
39 let req = apply_headers(build_agent().get(url), headers);
40 let body = req.call()?.body_mut().read_to_string()?;
41 Ok(body)
42}
43
44pub fn http_post(url: &str, body: &str) -> Result<String, StepError> {
46 http_post_with_headers(url, body, &[])
47}
48
49pub fn http_post_with_headers(
51 url: &str,
52 body: &str,
53 headers: &[(&str, &str)],
54) -> Result<String, StepError> {
55 let req = apply_headers(build_agent().post(url), headers);
56 let response = req.send(body)?.body_mut().read_to_string()?;
57 Ok(response)
58}
59
60pub fn http_post_json(url: &str, body: &serde_json::Value) -> Result<String, StepError> {
62 http_post_json_with_headers(url, body, &[])
63}
64
65pub fn http_post_json_with_headers(
67 url: &str,
68 body: &serde_json::Value,
69 headers: &[(&str, &str)],
70) -> Result<String, StepError> {
71 let req = apply_headers(build_agent().post(url), headers);
72 let response = req.send_json(body)?.body_mut().read_to_string()?;
73 Ok(response)
74}
75
76fn build_agent() -> Agent {
77 Agent::config_builder()
78 .timeout_global(Some(Duration::from_secs(5)))
79 .build()
80 .into()
81}
82
83fn apply_headers<B>(mut req: RequestBuilder<B>, headers: &[(&str, &str)]) -> RequestBuilder<B> {
84 req = req.header("User-Agent", "agent-line");
85 for &(k, v) in headers {
86 req = req.header(k, v);
87 }
88
89 req
90}
91
92#[cfg(test)]
93mod tests {
94 use serde_json::json;
95
96 use super::*;
97
98 fn spawn_echo_server() -> (String, std::thread::JoinHandle<String>) {
99 use std::io::{BufRead, BufReader, Read, Write};
100 use std::net::TcpListener;
101
102 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
103 let port = listener.local_addr().unwrap().port();
104 let url = format!("http://127.0.0.1:{port}/");
105
106 let handle = std::thread::spawn(move || {
107 let (stream, _) = listener.accept().unwrap();
108 let mut reader = BufReader::new(stream.try_clone().unwrap());
109
110 let mut request = String::new();
112 let mut content_length = 0;
113 loop {
114 let mut line = String::new();
115 reader.read_line(&mut line).unwrap();
116 if let Some(value) = line.to_lowercase().strip_prefix("content-length:") {
117 content_length = value.trim().parse().unwrap_or(0);
118 }
119 let end_of_headers = line == "\r\n" || line.is_empty();
120 request.push_str(&line);
121 if end_of_headers {
122 break;
123 }
124 }
125
126 if content_length > 0 {
128 let mut body = vec![0u8; content_length];
129 reader.read_exact(&mut body).unwrap();
130 request.push_str(&String::from_utf8_lossy(&body));
131 }
132
133 let mut stream = stream;
134 stream
135 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok")
136 .unwrap();
137 request
138 });
139
140 (url, handle)
141 }
142
143 #[test]
144 fn test_http_get_bad_url_returns_error() {
145 let result = http_get("http://localhost:1/nope");
146 assert!(result.is_err());
147 }
148
149 #[test]
150 fn test_http_post_bad_url_returns_error() {
151 let result = http_post("http://localhost:1/nope", "body content");
152 assert!(result.is_err());
153 }
154
155 #[test]
156 fn test_http_post_json_bad_url_returns_error() {
157 let body = serde_json::json!({"key": "value"});
158 let result = http_post_json("http://localhost:1/nope", &body);
159 assert!(result.is_err());
160 }
161
162 #[test]
163 fn test_get_sends_custom_headers() {
164 let (url, server) = spawn_echo_server();
165 let res = http_get_with_headers(&url, &[("X-Test", "hello")]);
166 let request = server.join().unwrap();
167 assert!(res.is_ok());
168 assert!(
169 request.to_lowercase().contains("x-test: hello"),
170 "header missing from request:\n{request}"
171 );
172 }
173
174 #[test]
175 fn test_post_sends_custom_headers() {
176 let (url, server) = spawn_echo_server();
177 let res = http_post_with_headers(&url, "test body", &[("X-Test", "bye")]);
178 let request = server.join().unwrap();
179 assert!(res.is_ok());
180 assert!(
181 request.to_lowercase().contains("x-test: bye"),
182 "header missing from request:\n{request}"
183 );
184 assert!(
185 request.contains("test body"),
186 "body missing from request:\n{request}"
187 );
188 }
189
190 #[test]
191 fn test_post_json_sends_custom_headers() {
192 let (url, server) = spawn_echo_server();
193 let body = json!({"name": "llm-agent"});
194 let res = http_post_json_with_headers(&url, &body, &[("X-Test", "cya")]);
195 let request = server.join().unwrap();
196 assert!(res.is_ok());
197 assert!(
198 request.to_lowercase().contains("x-test: cya"),
199 "header missing from request:\n{request}"
200 );
201 }
202}