Skip to main content

agentkit_http/
lib.rs

1//! HTTP transport trait and request/response types.
2//!
3//! Implementors satisfy [`HttpClient::execute`]; [`HttpRequestBuilder`] and
4//! [`HttpResponse`] carry the ergonomics (body encoding, header helpers,
5//! streaming). Enable the `reqwest-client` feature (default) for an
6//! `impl HttpClient for reqwest::Client`; disable it to compile trait-only.
7
8mod client;
9mod error;
10mod request;
11mod response;
12
13#[cfg(feature = "reqwest-client")]
14mod reqwest_impl;
15
16#[cfg(feature = "reqwest-middleware-client")]
17mod reqwest_middleware_impl;
18
19pub use client::{Http, HttpClient};
20pub use error::{BoxError, HttpError};
21pub use request::{HttpRequest, HttpRequestBuilder};
22pub use response::{BodyStream, HttpResponse};
23
24pub use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri, header};
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29    use async_trait::async_trait;
30    use bytes::Bytes;
31    use futures_util::stream;
32    use std::sync::Arc;
33    use std::sync::atomic::{AtomicUsize, Ordering};
34
35    struct StubClient {
36        calls: AtomicUsize,
37        status: StatusCode,
38        body: Bytes,
39        expected_body: Option<Bytes>,
40    }
41
42    #[async_trait]
43    impl HttpClient for StubClient {
44        async fn execute(&self, request: HttpRequest) -> Result<HttpResponse, HttpError> {
45            self.calls.fetch_add(1, Ordering::SeqCst);
46            if let Some(expected) = &self.expected_body {
47                assert_eq!(request.body.as_deref(), Some(expected.as_ref()));
48            }
49            let body = self.body.clone();
50            let stream = stream::once(async move { Ok::<_, HttpError>(body) });
51            Ok(HttpResponse::new(
52                self.status,
53                request.headers.clone(),
54                request.url.clone(),
55                Box::pin(stream),
56            ))
57        }
58    }
59
60    #[tokio::test]
61    async fn builder_sends_json_body_and_decodes_response() {
62        #[derive(serde::Serialize)]
63        struct Req {
64            name: &'static str,
65        }
66        #[derive(serde::Deserialize, Debug, PartialEq)]
67        struct Resp {
68            ok: bool,
69        }
70
71        let stub = StubClient {
72            calls: AtomicUsize::new(0),
73            status: StatusCode::OK,
74            body: Bytes::from_static(br#"{"ok":true}"#),
75            expected_body: Some(Bytes::from_static(br#"{"name":"agentkit"}"#)),
76        };
77        let http = Http::from_arc(Arc::new(stub));
78
79        let resp = http
80            .post("https://example.test/echo")
81            .bearer_auth("tok")
82            .json(&Req { name: "agentkit" })
83            .send()
84            .await
85            .expect("send");
86
87        assert_eq!(resp.status(), StatusCode::OK);
88        let auth = resp.headers().get(http::header::AUTHORIZATION).unwrap();
89        assert_eq!(auth, "Bearer tok");
90        let ct = resp.headers().get(http::header::CONTENT_TYPE).unwrap();
91        assert_eq!(ct, "application/json");
92
93        let decoded: Resp = resp.json().await.expect("json");
94        assert_eq!(decoded, Resp { ok: true });
95    }
96
97    #[tokio::test]
98    async fn error_for_status_flags_4xx() {
99        let stub = StubClient {
100            calls: AtomicUsize::new(0),
101            status: StatusCode::BAD_REQUEST,
102            body: Bytes::from_static(b"nope"),
103            expected_body: None,
104        };
105        let http = Http::from_arc(Arc::new(stub));
106
107        let resp = http.get("https://example.test").send().await.unwrap();
108        assert!(resp.error_for_status().is_err());
109    }
110}