LCOV - code coverage report
Current view: top level - proxy/src - http.rs (source / functions) Coverage Total Hit
Test: 42f947419473a288706e86ecdf7c2863d760d5d7.info Lines: 57.5 % 80 46
Test Date: 2024-08-02 21:34:27 Functions: 45.5 % 11 5

            Line data    Source code
       1              : //! HTTP client and server impls.
       2              : //! Other modules should use stuff from this module instead of
       3              : //! directly relying on deps like `reqwest` (think loose coupling).
       4              : 
       5              : pub mod health_server;
       6              : 
       7              : use std::time::Duration;
       8              : 
       9              : pub use reqwest::{Request, Response, StatusCode};
      10              : pub use reqwest_middleware::{ClientWithMiddleware, Error};
      11              : pub use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
      12              : 
      13              : use crate::{
      14              :     metrics::{ConsoleRequest, Metrics},
      15              :     url::ApiUrl,
      16              : };
      17              : use reqwest_middleware::RequestBuilder;
      18              : 
      19              : /// This is the preferred way to create new http clients,
      20              : /// because it takes care of observability (OpenTelemetry).
      21              : /// We deliberately don't want to replace this with a public static.
      22            2 : pub fn new_client() -> ClientWithMiddleware {
      23            2 :     let client = reqwest::ClientBuilder::new()
      24            2 :         .build()
      25            2 :         .expect("Failed to create http client");
      26            2 : 
      27            2 :     reqwest_middleware::ClientBuilder::new(client)
      28            2 :         .with(reqwest_tracing::TracingMiddleware::default())
      29            2 :         .build()
      30            2 : }
      31              : 
      32            0 : pub fn new_client_with_timeout(default_timout: Duration) -> ClientWithMiddleware {
      33            0 :     let timeout_client = reqwest::ClientBuilder::new()
      34            0 :         .timeout(default_timout)
      35            0 :         .build()
      36            0 :         .expect("Failed to create http client with timeout");
      37            0 : 
      38            0 :     let retry_policy =
      39            0 :         ExponentialBackoff::builder().build_with_total_retry_duration(default_timout);
      40            0 : 
      41            0 :     reqwest_middleware::ClientBuilder::new(timeout_client)
      42            0 :         .with(reqwest_tracing::TracingMiddleware::default())
      43            0 :         // As per docs, "This middleware always errors when given requests with streaming bodies".
      44            0 :         // That's all right because we only use this client to send `serde_json::RawValue`, which
      45            0 :         // is not a stream.
      46            0 :         //
      47            0 :         // ex-maintainer note:
      48            0 :         // this limitation can be fixed if streaming is necessary.
      49            0 :         // retries will still not be performed, but it wont error immediately
      50            0 :         .with(RetryTransientMiddleware::new_with_policy(retry_policy))
      51            0 :         .build()
      52            0 : }
      53              : 
      54              : /// Thin convenience wrapper for an API provided by an http endpoint.
      55              : #[derive(Debug, Clone)]
      56              : pub struct Endpoint {
      57              :     /// API's base URL.
      58              :     endpoint: ApiUrl,
      59              :     /// Connection manager with built-in pooling.
      60              :     client: ClientWithMiddleware,
      61              : }
      62              : 
      63              : impl Endpoint {
      64              :     /// Construct a new HTTP endpoint wrapper.
      65              :     /// Http client is not constructed under the hood so that it can be shared.
      66            4 :     pub fn new(endpoint: ApiUrl, client: impl Into<ClientWithMiddleware>) -> Self {
      67            4 :         Self {
      68            4 :             endpoint,
      69            4 :             client: client.into(),
      70            4 :         }
      71            4 :     }
      72              : 
      73              :     #[inline(always)]
      74            0 :     pub fn url(&self) -> &ApiUrl {
      75            0 :         &self.endpoint
      76            0 :     }
      77              : 
      78              :     /// Return a [builder](RequestBuilder) for a `GET` request,
      79              :     /// appending a single `path` segment to the base endpoint URL.
      80            4 :     pub fn get(&self, path: &str) -> RequestBuilder {
      81            4 :         let mut url = self.endpoint.clone();
      82            4 :         url.path_segments_mut().push(path);
      83            4 :         self.client.get(url.into_inner())
      84            4 :     }
      85              : 
      86              :     /// Execute a [request](reqwest::Request).
      87            0 :     pub async fn execute(&self, request: Request) -> Result<Response, Error> {
      88            0 :         let _timer = Metrics::get()
      89            0 :             .proxy
      90            0 :             .console_request_latency
      91            0 :             .start_timer(ConsoleRequest {
      92            0 :                 request: request.url().path(),
      93            0 :             });
      94            0 : 
      95            0 :         self.client.execute(request).await
      96            0 :     }
      97              : }
      98              : 
      99              : #[cfg(test)]
     100              : mod tests {
     101              :     use super::*;
     102              :     use reqwest::Client;
     103              : 
     104              :     #[test]
     105            2 :     fn optional_query_params() -> anyhow::Result<()> {
     106            2 :         let url = "http://example.com".parse()?;
     107            2 :         let endpoint = Endpoint::new(url, Client::new());
     108              : 
     109              :         // Validate that this pattern makes sense.
     110            2 :         let req = endpoint
     111            2 :             .get("frobnicate")
     112            2 :             .query(&[
     113            2 :                 ("foo", Some("10")), // should be just `foo=10`
     114            2 :                 ("bar", None),       // shouldn't be passed at all
     115            2 :             ])
     116            2 :             .build()?;
     117              : 
     118            2 :         assert_eq!(req.url().as_str(), "http://example.com/frobnicate?foo=10");
     119              : 
     120            2 :         Ok(())
     121            2 :     }
     122              : 
     123              :     #[test]
     124            2 :     fn uuid_params() -> anyhow::Result<()> {
     125            2 :         let url = "http://example.com".parse()?;
     126            2 :         let endpoint = Endpoint::new(url, Client::new());
     127              : 
     128            2 :         let req = endpoint
     129            2 :             .get("frobnicate")
     130            2 :             .query(&[("session_id", uuid::Uuid::nil())])
     131            2 :             .build()?;
     132              : 
     133            2 :         assert_eq!(
     134            2 :             req.url().as_str(),
     135            2 :             "http://example.com/frobnicate?session_id=00000000-0000-0000-0000-000000000000"
     136            2 :         );
     137              : 
     138            2 :         Ok(())
     139            2 :     }
     140              : }
        

Generated by: LCOV version 2.1-beta