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 : }
|