crux_http/client.rs
1use std::fmt;
2use std::sync::Arc;
3
4use crate::middleware::{Middleware, Next};
5use crate::protocol::{EffectSender, HttpResult, ProtocolRequestBuilder};
6use crate::{Config, Request, RequestBuilder, ResponseAsync, Result};
7use http_types::{Method, Url};
8
9/// An HTTP client, capable of sending `Request`s
10///
11/// Users should only interact with this type from middlewares - normal crux code should
12/// make use of the `Http` capability type instead.
13///
14/// # Examples
15///
16/// ```no_run
17/// use futures_util::future::BoxFuture;
18/// use crux_http::middleware::{Next, Middleware};
19/// use crux_http::{client::Client, Request, RequestBuilder, ResponseAsync, Result};
20/// use std::time;
21/// use std::sync::Arc;
22///
23/// // Fetches an authorization token prior to making a request
24/// fn fetch_auth<'a>(mut req: Request, client: Client, next: Next<'a>) -> BoxFuture<'a, Result<ResponseAsync>> {
25/// Box::pin(async move {
26/// let auth_token = client.get("https://httpbin.org/get")
27/// .await?
28/// .body_string()
29/// .await?;
30/// req.append_header("Authorization", format!("Bearer {auth_token}"));
31/// next.run(req, client).await
32/// })
33/// }
34/// ```
35pub struct Client {
36 config: Config,
37 effect_sender: Arc<dyn EffectSender + Send + Sync>,
38 /// Holds the middleware stack.
39 ///
40 /// Note(Fishrock123): We do actually want this structure.
41 /// The outer Arc allows us to clone in .send() without cloning the array.
42 /// The Vec allows us to add middleware at runtime.
43 /// The inner Arc-s allow us to implement Clone without sharing the vector with the parent.
44 /// We don't use a Mutex around the Vec here because adding a middleware during execution should be an error.
45 #[allow(clippy::rc_buffer)]
46 middleware: Arc<Vec<Arc<dyn Middleware>>>,
47}
48
49impl Clone for Client {
50 /// Clones the Client.
51 ///
52 /// This copies the middleware stack from the original, but shares
53 /// the `HttpClient` and http client config of the original.
54 /// Note that individual middleware in the middleware stack are
55 /// still shared by reference.
56 fn clone(&self) -> Self {
57 Self {
58 config: self.config.clone(),
59 effect_sender: Arc::clone(&self.effect_sender),
60 middleware: Arc::new(self.middleware.iter().cloned().collect()),
61 }
62 }
63}
64
65impl fmt::Debug for Client {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(f, "Client {{}}")
68 }
69}
70
71impl Client {
72 pub(crate) fn new<Sender>(sender: Sender) -> Self
73 where
74 Sender: EffectSender + Send + Sync + 'static,
75 {
76 Self {
77 config: Config::default(),
78 effect_sender: Arc::new(sender),
79 middleware: Arc::new(vec![]),
80 }
81 }
82
83 // This is currently dead code because there's no easy way to configure a client.
84 // TODO: fix that in some future PR
85 #[allow(dead_code)]
86 /// Push middleware onto the middleware stack.
87 ///
88 /// See the [middleware] submodule for more information on middleware.
89 ///
90 /// [middleware]: ../middleware/index.html
91 pub(crate) fn with(mut self, middleware: impl Middleware) -> Self {
92 let m = Arc::get_mut(&mut self.middleware)
93 .expect("Registering middleware is not possible after the Client has been used");
94 m.push(Arc::new(middleware));
95 self
96 }
97
98 /// Send a `Request` using this client.
99 pub async fn send(&self, req: impl Into<Request>) -> Result<ResponseAsync> {
100 let mut req: Request = req.into();
101 let middleware = self.middleware.clone();
102
103 let mw_stack = match req.take_middleware() {
104 Some(req_mw) => {
105 let mut mw = Vec::with_capacity(middleware.len() + req_mw.len());
106 mw.extend(middleware.iter().cloned());
107 mw.extend(req_mw);
108 Arc::new(mw)
109 }
110 None => middleware,
111 };
112
113 let next = Next::new(&mw_stack, &|req, client| {
114 Box::pin(async move {
115 let req = req
116 .into_protocol_request()
117 .await
118 .expect("Failed to create request");
119 match client.effect_sender.send(req).await {
120 HttpResult::Ok(res) => Ok(res.into()),
121 HttpResult::Err(e) => Err(e),
122 }
123 })
124 });
125
126 let client = Self {
127 config: self.config.clone(),
128 effect_sender: Arc::clone(&self.effect_sender),
129 // Erase the middleware stack for the Client accessible from within middleware.
130 // This avoids gratuitous circular borrow & logic issues.
131 middleware: Arc::new(vec![]),
132 };
133
134 let res = next.run(req, client).await?;
135 Ok(ResponseAsync::new(res.into()))
136 }
137
138 /// Submit a `Request` and get the response body as bytes.
139 pub async fn recv_bytes(&self, req: impl Into<Request>) -> Result<Vec<u8>> {
140 let mut res = self.send(req.into()).await?;
141 res.body_bytes().await
142 }
143
144 /// Submit a `Request` and get the response body as a string.
145 pub async fn recv_string(&self, req: impl Into<Request>) -> Result<String> {
146 let mut res = self.send(req.into()).await?;
147 res.body_string().await
148 }
149
150 /// Submit a `Request` and decode the response body from json into a struct.
151 pub async fn recv_json<T: serde::de::DeserializeOwned>(
152 &self,
153 req: impl Into<Request>,
154 ) -> Result<T> {
155 let mut res = self.send(req.into()).await?;
156 res.body_json::<T>().await
157 }
158
159 /// Submit a `Request` and decode the response body from form encoding into a struct.
160 ///
161 /// # Errors
162 ///
163 /// Any I/O error encountered while reading the body is immediately returned
164 /// as an `Err`.
165 ///
166 /// If the body cannot be interpreted as valid json for the target type `T`,
167 /// an `Err` is returned.
168 pub async fn recv_form<T: serde::de::DeserializeOwned>(
169 &self,
170 req: impl Into<Request>,
171 ) -> Result<T> {
172 let mut res = self.send(req.into()).await?;
173 res.body_form::<T>().await
174 }
175
176 /// Perform an HTTP `GET` request using the `Client` connection.
177 ///
178 /// # Panics
179 ///
180 /// This will panic if a malformed URL is passed.
181 ///
182 /// # Errors
183 ///
184 /// Returns errors from the middleware, http backend, and network sockets.
185 pub fn get(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
186 RequestBuilder::new_for_middleware(Method::Get, self.url(uri), self.clone())
187 }
188
189 /// Perform an HTTP `HEAD` request using the `Client` connection.
190 ///
191 /// # Panics
192 ///
193 /// This will panic if a malformed URL is passed.
194 ///
195 /// # Errors
196 ///
197 /// Returns errors from the middleware, http backend, and network sockets.
198 pub fn head(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
199 RequestBuilder::new_for_middleware(Method::Head, self.url(uri), self.clone())
200 }
201
202 /// Perform an HTTP `POST` request using the `Client` connection.
203 ///
204 /// # Panics
205 ///
206 /// This will panic if a malformed URL is passed.
207 ///
208 /// # Errors
209 ///
210 /// Returns errors from the middleware, http backend, and network sockets.
211 pub fn post(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
212 RequestBuilder::new_for_middleware(Method::Post, self.url(uri), self.clone())
213 }
214
215 /// Perform an HTTP `PUT` request using the `Client` connection.
216 ///
217 /// # Panics
218 ///
219 /// This will panic if a malformed URL is passed.
220 ///
221 /// # Errors
222 ///
223 /// Returns errors from the middleware, http backend, and network sockets.
224 pub fn put(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
225 RequestBuilder::new_for_middleware(Method::Put, self.url(uri), self.clone())
226 }
227
228 /// Perform an HTTP `DELETE` request using the `Client` connection.
229 ///
230 /// # Panics
231 ///
232 /// This will panic if a malformed URL is passed.
233 ///
234 /// # Errors
235 ///
236 /// Returns errors from the middleware, http backend, and network sockets.
237 pub fn delete(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
238 RequestBuilder::new_for_middleware(Method::Delete, self.url(uri), self.clone())
239 }
240
241 /// Perform an HTTP `CONNECT` request using the `Client` connection.
242 ///
243 /// # Panics
244 ///
245 /// This will panic if a malformed URL is passed.
246 ///
247 /// # Errors
248 ///
249 /// Returns errors from the middleware, http backend, and network sockets.
250 pub fn connect(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
251 RequestBuilder::new_for_middleware(Method::Connect, self.url(uri), self.clone())
252 }
253
254 /// Perform an HTTP `OPTIONS` request using the `Client` connection.
255 ///
256 /// # Panics
257 ///
258 /// This will panic if a malformed URL is passed.
259 ///
260 /// # Errors
261 ///
262 /// Returns errors from the middleware, http backend, and network sockets.
263 pub fn options(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
264 RequestBuilder::new_for_middleware(Method::Options, self.url(uri), self.clone())
265 }
266
267 /// Perform an HTTP `TRACE` request using the `Client` connection.
268 ///
269 /// # Panics
270 ///
271 /// This will panic if a malformed URL is passed.
272 ///
273 /// # Errors
274 ///
275 /// Returns errors from the middleware, http backend, and network sockets.
276 pub fn trace(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
277 RequestBuilder::new_for_middleware(Method::Trace, self.url(uri), self.clone())
278 }
279
280 /// Perform an HTTP `PATCH` request using the `Client` connection.
281 ///
282 /// # Panics
283 ///
284 /// This will panic if a malformed URL is passed.
285 ///
286 /// # Errors
287 ///
288 /// Returns errors from the middleware, http backend, and network sockets.
289 pub fn patch(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
290 RequestBuilder::new_for_middleware(Method::Patch, self.url(uri), self.clone())
291 }
292
293 /// Perform a HTTP request with the given verb using the `Client` connection.
294 ///
295 /// # Panics
296 ///
297 /// This will panic if a malformed URL is passed.
298 ///
299 /// # Errors
300 ///
301 /// Returns errors from the middleware, http backend, and network sockets.
302 pub fn request(&self, verb: Method, uri: impl AsRef<str>) -> RequestBuilder<()> {
303 RequestBuilder::new_for_middleware(verb, self.url(uri), self.clone())
304 }
305
306 /// Get the current configuration.
307 pub fn config(&self) -> &Config {
308 &self.config
309 }
310
311 // private function to generate a url based on the base_path
312 fn url(&self, uri: impl AsRef<str>) -> Url {
313 match &self.config.base_url {
314 None => uri.as_ref().parse().unwrap(),
315 Some(base) => base.join(uri.as_ref()).unwrap(),
316 }
317 }
318}
319
320#[cfg(test)]
321mod client_tests {
322 use super::Client;
323 use crate::protocol::{HttpRequest, HttpResponse};
324 use crate::testing::FakeShell;
325
326 #[futures_test::test]
327 async fn an_http_get() {
328 let mut shell = FakeShell::default();
329 shell.provide_response(HttpResponse::ok().body("Hello World!").build());
330
331 let client = Client::new(shell.clone());
332
333 let mut response = client.get("https://example.com").await.unwrap();
334 assert_eq!(response.body_string().await.unwrap(), "Hello World!");
335
336 assert_eq!(
337 shell.take_requests_received(),
338 vec![HttpRequest::get("https://example.com/").build()]
339 )
340 }
341}