Skip to main content

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, RawResponse, Request, RequestBuilder, Result};
7use http::Method;
8use url::Url;
9
10/// An HTTP client, capable of sending `Request`s
11///
12/// Users should only interact with this type from middlewares - normal crux code should
13/// make use of the `Http` capability type instead.
14///
15/// # Examples
16///
17/// ```no_run
18/// use futures_util::future::BoxFuture;
19/// use crux_http::middleware::{Next, Middleware};
20/// use crux_http::{client::Client, Request, RequestBuilder, RawResponse, Result};
21/// use crux_http::http::HeaderValue;
22/// use std::time;
23/// use std::sync::Arc;
24///
25/// // Fetches an authorization token prior to making a request
26/// fn fetch_auth<'a>(mut req: Request, client: Client, next: Next<'a>) -> BoxFuture<'a, Result<RawResponse>> {
27///     Box::pin(async move {
28///         let auth_token = client.get("https://httpbin.org/get")
29///             .await?
30///             .body_string()?;
31///         let value = HeaderValue::from_str(&format!("Bearer {auth_token}")).expect("valid token");
32///         req.append_header("Authorization", value);
33///         next.run(req, client).await
34///     })
35/// }
36/// ```
37pub struct Client {
38    config: Config,
39    effect_sender: Arc<dyn EffectSender + Send + Sync>,
40    /// Holds the middleware stack.
41    ///
42    /// Note(Fishrock123): We do actually want this structure.
43    /// The outer Arc allows us to clone in `.send()` without cloning the array.
44    /// The Vec allows us to add middleware at runtime.
45    /// The inner Arc-s allow us to implement Clone without sharing the vector with the parent.
46    /// We don't use a Mutex around the Vec here because adding a middleware during execution should be an error.
47    #[allow(clippy::rc_buffer)]
48    middleware: Arc<Vec<Arc<dyn Middleware>>>,
49}
50
51impl Clone for Client {
52    /// Clones the Client.
53    ///
54    /// This copies the middleware stack from the original, but shares
55    /// the `HttpClient` and http client config of the original.
56    /// Note that individual middleware in the middleware stack are
57    /// still shared by reference.
58    fn clone(&self) -> Self {
59        Self {
60            config: self.config.clone(),
61            effect_sender: Arc::clone(&self.effect_sender),
62            middleware: Arc::new(self.middleware.iter().cloned().collect()),
63        }
64    }
65}
66
67impl fmt::Debug for Client {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "Client {{}}")
70    }
71}
72
73impl Client {
74    #[cfg(test)]
75    pub(crate) fn new<Sender>(sender: Sender) -> Self
76    where
77        Sender: EffectSender + Send + Sync + 'static,
78    {
79        Self {
80            config: Config::default(),
81            effect_sender: Arc::new(sender),
82            middleware: Arc::new(vec![]),
83        }
84    }
85
86    #[cfg(test)]
87    pub(crate) fn new_with_config<Sender>(sender: Sender, config: Config) -> Self
88    where
89        Sender: EffectSender + Send + Sync + 'static,
90    {
91        Self {
92            config,
93            effect_sender: Arc::new(sender),
94            middleware: Arc::new(vec![]),
95        }
96    }
97
98    // This is currently dead code because there's no easy way to configure a client.
99    // TODO: fix that in some future PR
100    #[allow(dead_code)]
101    /// Push middleware onto the middleware stack.
102    ///
103    /// See the [middleware] submodule for more information on middleware.
104    ///
105    /// [middleware]: ../middleware/index.html
106    pub(crate) fn with(mut self, middleware: impl Middleware) -> Self {
107        let m = Arc::get_mut(&mut self.middleware)
108            .expect("Registering middleware is not possible after the Client has been used");
109        m.push(Arc::new(middleware));
110        self
111    }
112
113    /// Send a `Request` using this client.
114    ///
115    /// # Errors
116    /// Errors if there is an error sending the request.
117    ///
118    /// # Panics
119    /// Panics if we can't create an HTTP request.
120    pub async fn send(&self, request: impl Into<Request>) -> Result<RawResponse> {
121        let mut request: Request = request.into();
122
123        // Apply per-client default headers for any name not already set on the request.
124        // keys() yields one entry per value (including duplicates), so we use is_some()
125        // to skip a name once we've already appended all its values from config.
126        for name in self.config.headers.keys() {
127            if request.header(name).is_none() {
128                for value in self.config.headers.get_all(name) {
129                    request.append_header(name.clone(), value.clone());
130                }
131            }
132        }
133
134        let middleware = self.middleware.clone();
135
136        let mw_stack = match request.take_middleware() {
137            Some(req_mw) => {
138                let mut mw = Vec::with_capacity(middleware.len() + req_mw.len());
139                mw.extend(middleware.iter().cloned());
140                mw.extend(req_mw);
141                Arc::new(mw)
142            }
143            None => middleware,
144        };
145
146        let next = Next::new(&mw_stack, &|request, client| {
147            Box::pin(async move {
148                let request = request
149                    .into_protocol_request()
150                    .expect("Failed to create request");
151                match client.effect_sender.send(request).await {
152                    HttpResult::Ok(response) => response.try_into(),
153                    HttpResult::Err(e) => Err(e),
154                }
155            })
156        });
157
158        let client = Self {
159            config: self.config.clone(),
160            effect_sender: Arc::clone(&self.effect_sender),
161            // Erase the middleware stack for the Client accessible from within middleware.
162            // This avoids gratuitous circular borrow & logic issues.
163            middleware: Arc::new(vec![]),
164        };
165
166        let response = next.run(request, client).await?;
167        Ok(response)
168    }
169
170    /// Submit a `Request` and get the response body as bytes.
171    ///
172    /// # Errors
173    /// Errors if there is an error sending the request
174    pub async fn recv_bytes(&self, request: impl Into<Request>) -> Result<Vec<u8>> {
175        let mut response = self.send(request.into()).await?;
176        response.body_bytes()
177    }
178
179    /// Submit a `Request` and get the response body as a string.
180    ///
181    /// # Errors
182    /// Errors if there is an error sending the request
183    pub async fn recv_string(&self, request: impl Into<Request>) -> Result<String> {
184        let mut response = self.send(request.into()).await?;
185        response.body_string()
186    }
187
188    /// Submit a `Request` and decode the response body from json into a struct.
189    ///
190    /// # Errors
191    /// Errors if there is an error sending the request
192    pub async fn recv_json<T: serde::de::DeserializeOwned>(
193        &self,
194        request: impl Into<Request>,
195    ) -> Result<T> {
196        let mut response = self.send(request.into()).await?;
197        response.body_json::<T>()
198    }
199
200    /// Submit a `Request` and decode the response body from form encoding into a struct.
201    ///
202    /// # Errors
203    ///
204    /// Any I/O error encountered while reading the body is immediately returned
205    /// as an `Err`.
206    ///
207    /// If the body cannot be interpreted as valid json for the target type `T`,
208    /// an `Err` is returned.
209    pub async fn recv_form<T: serde::de::DeserializeOwned>(
210        &self,
211        request: impl Into<Request>,
212    ) -> Result<T> {
213        let mut response = self.send(request.into()).await?;
214        response.body_form::<T>()
215    }
216
217    /// Perform an HTTP `GET` request using the `Client` connection.
218    ///
219    /// # Panics
220    ///
221    /// This will panic if a malformed URL is passed.
222    ///
223    /// # Errors
224    ///
225    /// Returns errors from the middleware, http backend, and network sockets.
226    pub fn get(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
227        RequestBuilder::new_for_middleware(Method::GET, self.url(uri), self.clone())
228    }
229
230    /// Perform an HTTP `HEAD` request using the `Client` connection.
231    ///
232    /// # Panics
233    ///
234    /// This will panic if a malformed URL is passed.
235    ///
236    /// # Errors
237    ///
238    /// Returns errors from the middleware, http backend, and network sockets.
239    pub fn head(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
240        RequestBuilder::new_for_middleware(Method::HEAD, self.url(uri), self.clone())
241    }
242
243    /// Perform an HTTP `POST` request using the `Client` connection.
244    ///
245    /// # Panics
246    ///
247    /// This will panic if a malformed URL is passed.
248    ///
249    /// # Errors
250    ///
251    /// Returns errors from the middleware, http backend, and network sockets.
252    pub fn post(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
253        RequestBuilder::new_for_middleware(Method::POST, self.url(uri), self.clone())
254    }
255
256    /// Perform an HTTP `PUT` request using the `Client` connection.
257    ///
258    /// # Panics
259    ///
260    /// This will panic if a malformed URL is passed.
261    ///
262    /// # Errors
263    ///
264    /// Returns errors from the middleware, http backend, and network sockets.
265    pub fn put(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
266        RequestBuilder::new_for_middleware(Method::PUT, self.url(uri), self.clone())
267    }
268
269    /// Perform an HTTP `DELETE` request using the `Client` connection.
270    ///
271    /// # Panics
272    ///
273    /// This will panic if a malformed URL is passed.
274    ///
275    /// # Errors
276    ///
277    /// Returns errors from the middleware, http backend, and network sockets.
278    pub fn delete(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
279        RequestBuilder::new_for_middleware(Method::DELETE, self.url(uri), self.clone())
280    }
281
282    /// Perform an HTTP `CONNECT` request using the `Client` connection.
283    ///
284    /// # Panics
285    ///
286    /// This will panic if a malformed URL is passed.
287    ///
288    /// # Errors
289    ///
290    /// Returns errors from the middleware, http backend, and network sockets.
291    pub fn connect(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
292        RequestBuilder::new_for_middleware(Method::CONNECT, self.url(uri), self.clone())
293    }
294
295    /// Perform an HTTP `OPTIONS` request using the `Client` connection.
296    ///
297    /// # Panics
298    ///
299    /// This will panic if a malformed URL is passed.
300    ///
301    /// # Errors
302    ///
303    /// Returns errors from the middleware, http backend, and network sockets.
304    pub fn options(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
305        RequestBuilder::new_for_middleware(Method::OPTIONS, self.url(uri), self.clone())
306    }
307
308    /// Perform an HTTP `TRACE` request using the `Client` connection.
309    ///
310    /// # Panics
311    ///
312    /// This will panic if a malformed URL is passed.
313    ///
314    /// # Errors
315    ///
316    /// Returns errors from the middleware, http backend, and network sockets.
317    pub fn trace(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
318        RequestBuilder::new_for_middleware(Method::TRACE, self.url(uri), self.clone())
319    }
320
321    /// Perform an HTTP `PATCH` request using the `Client` connection.
322    ///
323    /// # Panics
324    ///
325    /// This will panic if a malformed URL is passed.
326    ///
327    /// # Errors
328    ///
329    /// Returns errors from the middleware, http backend, and network sockets.
330    pub fn patch(&self, uri: impl AsRef<str>) -> RequestBuilder<()> {
331        RequestBuilder::new_for_middleware(Method::PATCH, self.url(uri), self.clone())
332    }
333
334    /// Perform a HTTP request with the given verb using the `Client` connection.
335    ///
336    /// # Panics
337    ///
338    /// This will panic if a malformed URL is passed.
339    ///
340    /// # Errors
341    ///
342    /// Returns errors from the middleware, http backend, and network sockets.
343    pub fn request(&self, verb: Method, uri: impl AsRef<str>) -> RequestBuilder<()> {
344        RequestBuilder::new_for_middleware(verb, self.url(uri), self.clone())
345    }
346
347    /// Get the current configuration.
348    #[must_use]
349    #[allow(clippy::missing_const_for_fn)]
350    pub fn config(&self) -> &Config {
351        &self.config
352    }
353
354    // private function to generate a url based on the base_path
355    fn url(&self, uri: impl AsRef<str>) -> Url {
356        self.config.base_url.as_ref().map_or_else(
357            || uri.as_ref().parse().unwrap(),
358            |base| base.join(uri.as_ref()).unwrap(),
359        )
360    }
361}
362
363#[cfg(test)]
364mod client_tests {
365    use super::Client;
366    use crate::protocol::{HttpRequest, HttpResponse};
367    use crate::testing::FakeShell;
368
369    #[futures_test::test]
370    async fn an_http_get() {
371        let shell = FakeShell::default();
372        shell.provide_response(HttpResponse::ok().body("Hello World!").build());
373
374        let client = Client::new(shell.clone());
375
376        let mut response = client.get("https://example.com").await.unwrap();
377        assert_eq!(response.body_string().unwrap(), "Hello World!");
378
379        assert_eq!(
380            shell.take_requests_received(),
381            vec![HttpRequest::get("https://example.com/").build()]
382        );
383    }
384
385    #[futures_test::test]
386    async fn config_headers_are_sent_with_every_request() {
387        let shell = FakeShell::default();
388        shell.provide_response(HttpResponse::ok().build());
389
390        let config = crate::Config::default()
391            .add_header("x-api-key", "secret")
392            .unwrap();
393        let client = Client::new_with_config(shell.clone(), config);
394
395        client.get("https://example.com").await.unwrap();
396
397        let reqs = shell.take_requests_received();
398        assert_eq!(reqs.len(), 1);
399        assert!(
400            reqs[0]
401                .headers
402                .iter()
403                .any(|h| h.name == "x-api-key" && h.value == "secret"),
404            "x-api-key config header must appear in the outgoing request"
405        );
406    }
407
408    #[futures_test::test]
409    async fn per_request_header_takes_precedence_over_config_header() {
410        let shell = FakeShell::default();
411        shell.provide_response(HttpResponse::ok().build());
412
413        let config = crate::Config::default()
414            .add_header("x-version", "config-value")
415            .unwrap();
416        let client = Client::new_with_config(shell.clone(), config);
417
418        // Per-request header for the same name.
419        let mut req =
420            crate::Request::new(http::Method::GET, "https://example.com".parse().unwrap());
421        req.insert_header("x-version", http::HeaderValue::from_static("request-value"));
422        client.send(req).await.unwrap();
423
424        let reqs = shell.take_requests_received();
425        let values: Vec<&str> = reqs[0]
426            .headers
427            .iter()
428            .filter(|h| h.name == "x-version")
429            .map(|h| h.value.as_str())
430            .collect();
431
432        assert_eq!(values, ["request-value"], "per-request header must win");
433    }
434
435    #[futures_test::test]
436    async fn recv_bytes_returns_body() {
437        let shell = FakeShell::default();
438        shell.provide_response(HttpResponse::ok().body("bytes").build());
439        let client = Client::new(shell);
440        let bytes = client
441            .recv_bytes(crate::Request::new(
442                http::Method::GET,
443                "https://example.com".parse().unwrap(),
444            ))
445            .await
446            .unwrap();
447        assert_eq!(bytes, b"bytes");
448    }
449
450    #[futures_test::test]
451    async fn recv_string_returns_body() {
452        let shell = FakeShell::default();
453        shell.provide_response(HttpResponse::ok().body("hello").build());
454        let client = Client::new(shell);
455        let text = client
456            .recv_string(crate::Request::new(
457                http::Method::GET,
458                "https://example.com".parse().unwrap(),
459            ))
460            .await
461            .unwrap();
462        assert_eq!(text, "hello");
463    }
464
465    #[futures_test::test]
466    async fn recv_json_deserializes_body() {
467        #[derive(serde::Deserialize, PartialEq, Debug)]
468        struct Payload {
469            value: u32,
470        }
471        let shell = FakeShell::default();
472        shell.provide_response(
473            HttpResponse::ok()
474                .header("content-type", "application/json")
475                .json(serde_json::json!({"value": 42}))
476                .build(),
477        );
478        let client = Client::new(shell);
479        let payload: Payload = client
480            .recv_json(crate::Request::new(
481                http::Method::GET,
482                "https://example.com".parse().unwrap(),
483            ))
484            .await
485            .unwrap();
486        assert_eq!(payload, Payload { value: 42 });
487    }
488
489    #[futures_test::test]
490    async fn recv_form_deserializes_body() {
491        #[derive(serde::Deserialize, PartialEq, Debug)]
492        struct Payload {
493            key: String,
494        }
495        let shell = FakeShell::default();
496        shell.provide_response(HttpResponse::ok().body("key=val").build());
497        let client = Client::new(shell);
498        let payload: Payload = client
499            .recv_form(crate::Request::new(
500                http::Method::GET,
501                "https://example.com".parse().unwrap(),
502            ))
503            .await
504            .unwrap();
505        assert_eq!(payload, Payload { key: "val".into() });
506    }
507}