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#[derive(Debug, Clone)]
22pub struct Client {
23 config: Config,
24}
25
26impl Client {
27 pub fn new(config: Config) -> Self {
29 Self { config }
30 }
31
32 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 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}