Skip to main content

hyli_net/
http.rs

1use std::{future::Future, time::Duration};
2
3use anyhow::{Context, Result};
4#[cfg(feature = "instrumentation")]
5use axum::http;
6use bytes::Buf;
7use http_body_util::BodyExt;
8use hyper::{body::Incoming, client::conn::http1, Method, Request, Response, Uri};
9use hyper_util::rt::TokioIo;
10use serde::{de::DeserializeOwned, Serialize};
11use tracing::{error, warn};
12
13pub use hyper::http::StatusCode;
14
15pub enum ContentType {
16    Text,
17    Json,
18    OctetStream,
19}
20
21#[derive(Clone)]
22pub struct HttpClient {
23    pub url: Uri,
24    pub api_key: Option<String>,
25    pub retry: Option<(usize, Duration)>,
26}
27impl HttpClient {
28    #[cfg_attr(
29        feature = "instrumentation",
30        tracing::instrument(skip(self, body, content_type))
31    )]
32    pub async fn request<T>(
33        &self,
34        endpoint: &str,
35        method: Method,
36        content_type: ContentType,
37        body: Option<&T>,
38    ) -> anyhow::Result<Response<Incoming>>
39    where
40        T: Serialize,
41    {
42        let full_url = format!("{}{}", &self.url, endpoint);
43        let uri: Uri = full_url.parse().context("Parsing URI")?;
44
45        let authority = uri.authority().context("URI Authority")?.clone();
46        let host = authority.host().to_string();
47        let port = authority.port_u16().unwrap_or(80);
48        let addr = format!("{host}:{port}");
49        let stream = crate::net::TcpStream::connect(addr)
50            .await
51            .context("TCP connection")?;
52        let io = TokioIo::new(stream);
53
54        let (mut sender, connection) = http1::handshake(io).await.context("Handshake HTTP/1")?;
55
56        tokio::spawn(async move {
57            if let Err(e) = connection.await {
58                error!("Connection error {:?}", e);
59            }
60        });
61
62        let content_type_header = match content_type {
63            ContentType::Text => "application/text",
64            ContentType::Json => "application/json",
65            ContentType::OctetStream => "application/octet-stream",
66        };
67
68        let mut req_builder = Request::builder()
69            .method(method)
70            .uri(uri)
71            .header(hyper::header::CONTENT_TYPE, content_type_header);
72
73        if let Some(ref key) = self.api_key {
74            req_builder = req_builder.header("X-API-KEY", key);
75        }
76
77        #[cfg(feature = "instrumentation")]
78        if let Some(headers) = req_builder.headers_mut() {
79            opentelemetry::global::get_text_map_propagator(|propagator| {
80                use tracing_opentelemetry::OpenTelemetrySpanExt;
81                let context = tracing::Span::current().context();
82                propagator.inject_context(&context, &mut HeaderMapInjector(headers))
83            });
84        }
85
86        let request = if let Some(b) = body {
87            let json_body = serde_json::to_string(b).context("Serializing request body")?;
88
89            req_builder.body(json_body).context("Building request")?
90        } else {
91            req_builder.body("".to_string())?
92        };
93
94        let response = sender
95            .send_request(request)
96            .await
97            .context("Sending request")?;
98
99        if !response.status().is_success() {
100            let body = Self::parse_response_text(response).await?;
101            anyhow::bail!(body);
102        }
103
104        Ok(response)
105    }
106
107    async fn parse_response_text(response: Response<Incoming>) -> anyhow::Result<String> {
108        let str_bytes = Self::parse_response_bytes(response).await?;
109        let str = String::from_utf8(str_bytes.to_vec())?;
110
111        Ok(str)
112    }
113
114    async fn parse_response_bytes(response: Response<Incoming>) -> anyhow::Result<bytes::Bytes> {
115        let body = response.into_body();
116        let response = body.collect().await.context("Collecting body bytes")?;
117        Ok(response.to_bytes())
118    }
119
120    async fn parse_response_json<T: serde::de::DeserializeOwned>(
121        response: Response<Incoming>,
122    ) -> anyhow::Result<T> {
123        let body = response.into_body();
124
125        let response = body
126            .collect()
127            .await
128            .context("Collecting body bytes")?
129            .aggregate();
130
131        let result =
132            serde_json::from_reader(response.reader()).context("Deserializing response body")?;
133        Ok(result)
134    }
135
136    async fn retry<F, Fut, R>(&self, do_request: F) -> Result<R>
137    where
138        F: Fn() -> Fut,
139        Fut: Future<Output = anyhow::Result<R>>,
140    {
141        match self.retry {
142            Some((n, duration)) => {
143                let mut inner_n = n;
144
145                loop {
146                    match do_request().await {
147                        Ok(res) => break Ok(res),
148                        Err(e) if inner_n > 0 => {
149                            warn!(
150                                "Error when doing request, waiting {} millis before retrying: {}",
151                                duration.as_millis(),
152                                e
153                            );
154                            inner_n -= 1;
155                            tokio::time::sleep(duration).await;
156                        }
157                        Err(e) => {
158                            // Stop retrying
159                            break Err(e).context("Client errored after {} retries, stopping now.");
160                        }
161                    }
162                }
163            }
164            None => do_request().await,
165        }
166    }
167
168    pub async fn get<R>(&self, endpoint: &str) -> anyhow::Result<R>
169    where
170        R: DeserializeOwned,
171    {
172        let do_request = async || {
173            self.request::<String>(endpoint, Method::GET, ContentType::Json, None)
174                .await
175        };
176        let response = self.retry(do_request).await?;
177        Self::parse_response_json(response).await
178    }
179
180    pub async fn get_str(&self, endpoint: &str) -> anyhow::Result<String> {
181        let do_request = async || {
182            self.request::<String>(endpoint, Method::GET, ContentType::OctetStream, None)
183                .await
184        };
185        let response = self.retry(do_request).await?;
186        Self::parse_response_text(response).await
187    }
188
189    pub async fn get_bytes(&self, endpoint: &str) -> anyhow::Result<bytes::Bytes> {
190        let do_request = async || {
191            self.request::<String>(endpoint, Method::GET, ContentType::Text, None)
192                .await
193        };
194        let response = self.retry(do_request).await?;
195        Self::parse_response_bytes(response).await
196    }
197
198    pub async fn post_json<T, R>(&self, endpoint: &str, body: &T) -> anyhow::Result<R>
199    where
200        R: DeserializeOwned,
201        T: Serialize,
202    {
203        let do_request = async || {
204            self.request::<T>(endpoint, Method::POST, ContentType::Json, Some(body))
205                .await
206        };
207        let response = self.retry(do_request).await?;
208        Self::parse_response_json(response).await
209    }
210}
211
212#[cfg(feature = "instrumentation")]
213struct HeaderMapInjector<'a>(&'a mut http::HeaderMap);
214
215#[cfg(feature = "instrumentation")]
216impl opentelemetry::propagation::Injector for HeaderMapInjector<'_> {
217    fn set(&mut self, key: &str, value: String) {
218        use axum::http::{HeaderName, HeaderValue};
219
220        match HeaderName::from_bytes(key.as_bytes()) {
221            Ok(key) => match HeaderValue::try_from(&value) {
222                Ok(value) => {
223                    self.0.insert(key, value);
224                }
225
226                Err(error) => warn!(value, error =? error, "parse metadata value"),
227            },
228
229            Err(error) => warn!(key, error =? error, "parse metadata key"),
230        }
231    }
232}