Skip to main content

bimp_net/
client.rs

1use std::fmt::Display;
2
3use bytes::Bytes;
4use http::{Request, Response};
5use http_body_util::BodyExt;
6use hyper::body::Body as HyperBody;
7use tokio::sync::{mpsc, oneshot};
8
9use crate::body::Body;
10use crate::config::Config;
11use crate::error::Error;
12use crate::response::CollectedResponse;
13use crate::transfer::CurlTransfer;
14
15const BODY_CHANNEL_CAPACITY: usize = 16;
16
17/// HTTP client that executes requests with `libcurl-impersonate`.
18///
19/// A client is cheap to clone. Each request creates an isolated curl easy
20/// handle configured from the stored [`Config`].
21#[derive(Debug, Clone)]
22pub struct Client {
23    config: Config,
24}
25
26impl Client {
27    /// Creates a new client with the provided configuration.
28    pub fn new(config: Config) -> Self {
29        Self { config }
30    }
31
32    /// Sends a request and returns a streaming response body.
33    ///
34    /// The response resolves once final response headers are available. Body
35    /// bytes continue to arrive through [`Body`].
36    pub async fn send<B>(&self, request: Request<B>) -> Result<Response<Body>, Error>
37    where
38        B: HyperBody<Data = Bytes> + Send + 'static,
39        B::Error: Display,
40    {
41        self.send_buffered(collect_request_body(request).await?)
42            .await
43    }
44
45    async fn send_buffered(
46        &self,
47        request: Request<Option<Vec<u8>>>,
48    ) -> Result<Response<Body>, Error> {
49        let transfer = CurlTransfer::new(request, self.config.clone())?;
50        let (response_sender, response_receiver) = oneshot::channel();
51        let (body_sender, body_receiver) = mpsc::channel(BODY_CHANNEL_CAPACITY);
52
53        std::thread::Builder::new()
54            .name("bimp-curl".to_string())
55            .spawn(move || transfer.perform_streaming(response_sender, body_sender, body_receiver))
56            .map_err(|error| Error::Initialization(error.to_string()))?;
57
58        response_receiver
59            .await
60            .map_err(|_| Error::Network("curl worker exited before response headers".to_string()))?
61    }
62
63    /// Sends a request and collects the full response body into memory.
64    ///
65    /// This is convenient for small documents and tests. Use [`Self::send`] for
66    /// large responses or incremental consumption.
67    pub fn send_collect(
68        &self,
69        request: Request<Option<Vec<u8>>>,
70    ) -> Result<CollectedResponse, Error> {
71        CurlTransfer::new(request, self.config.clone())?.perform_collect()
72    }
73}
74
75async fn collect_request_body<B>(request: Request<B>) -> Result<Request<Option<Vec<u8>>>, Error>
76where
77    B: HyperBody<Data = Bytes> + Send + 'static,
78    B::Error: Display,
79{
80    let (parts, body) = request.into_parts();
81    let bytes = body
82        .collect()
83        .await
84        .map_err(|error| Error::InvalidRequest(format!("failed to collect request body: {error}")))?
85        .to_bytes();
86    let body = (!bytes.is_empty()).then(|| bytes.to_vec());
87    Ok(Request::from_parts(parts, body))
88}
89
90#[cfg(test)]
91mod tests {
92    use bytes::Bytes;
93    use http::Request;
94    use http_body_util::{Empty, Full};
95    use tokio::runtime::Builder;
96
97    use super::collect_request_body;
98
99    #[test]
100    fn collects_empty_hyper_body_as_none() {
101        let runtime = Builder::new_current_thread().build().unwrap();
102        let request = Request::builder()
103            .uri("https://example.com/")
104            .body(Empty::<Bytes>::new())
105            .unwrap();
106
107        let request = runtime.block_on(collect_request_body(request)).unwrap();
108
109        assert_eq!(request.uri(), "https://example.com/");
110        assert!(request.body().is_none());
111    }
112
113    #[test]
114    fn collects_full_hyper_body_as_bytes() {
115        let runtime = Builder::new_current_thread().build().unwrap();
116        let request = Request::builder()
117            .method("POST")
118            .uri("https://example.com/")
119            .body(Full::new(Bytes::from_static(b"hello")))
120            .unwrap();
121
122        let request = runtime.block_on(collect_request_body(request)).unwrap();
123
124        assert_eq!(request.method(), "POST");
125        assert_eq!(request.body().as_deref(), Some(&b"hello"[..]));
126    }
127}