use std::fmt::Debug;
use async_trait::async_trait;
use bytes::Bytes;
use http::{Request, Response};
use opentelemetry::{
propagation::{Extractor, Injector},
trace::TraceError,
};
pub struct HeaderInjector<'a>(pub &'a mut http::HeaderMap);
impl<'a> Injector for HeaderInjector<'a> {
fn set(&mut self, key: &str, value: String) {
if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) {
if let Ok(val) = http::header::HeaderValue::from_str(&value) {
self.0.insert(name, val);
}
}
}
}
pub struct HeaderExtractor<'a>(pub &'a http::HeaderMap);
impl<'a> Extractor for HeaderExtractor<'a> {
fn get(&self, key: &str) -> Option<&str> {
self.0.get(key).and_then(|value| value.to_str().ok())
}
fn keys(&self) -> Vec<&str> {
self.0
.keys()
.map(|value| value.as_str())
.collect::<Vec<_>>()
}
}
pub type HttpError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[async_trait]
pub trait HttpClient: Debug + Send + Sync {
async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError>;
}
#[cfg(feature = "reqwest")]
mod reqwest {
use super::{async_trait, Bytes, HttpClient, HttpError, Request, Response};
use std::convert::TryInto;
#[async_trait]
impl HttpClient for reqwest::Client {
async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError> {
let request = request.try_into()?;
let response = self.execute(request).await?;
Ok(Response::builder()
.status(response.status())
.body(response.bytes().await?)?)
}
}
#[async_trait]
impl HttpClient for reqwest::blocking::Client {
async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError> {
let request = request.try_into()?;
let response = self.execute(request)?;
Ok(Response::builder()
.status(response.status())
.body(response.bytes()?)?)
}
}
}
#[cfg(feature = "surf")]
mod surf {
use super::{async_trait, Bytes, HttpClient, HttpError, Request, Response};
#[async_trait]
impl HttpClient for surf::Client {
async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError> {
let (parts, body) = request.into_parts();
let method = parts.method.as_str().parse()?;
let uri = parts.uri.to_string().parse()?;
let mut request_builder = surf::Request::builder(method, uri).body(body);
let mut prev_name = None;
for (new_name, value) in parts.headers.into_iter() {
let name = new_name.or(prev_name).expect("the first time new_name should be set and from then on we always have a prev_name");
request_builder = request_builder.header(name.as_str(), value.to_str()?);
prev_name = Some(name);
}
let mut response = self.send(request_builder).await?;
Ok(Response::builder()
.status(response.status() as u16)
.body(response.body_bytes().await?.into())?)
}
}
}
#[cfg(feature = "isahc")]
mod isahc {
use super::{async_trait, Bytes, HttpClient, HttpError, Request, Response};
use isahc::AsyncReadResponseExt;
use std::convert::TryInto as _;
#[async_trait]
impl HttpClient for isahc::HttpClient {
async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError> {
let mut response = self.send_async(request).await?;
let mut bytes = Vec::with_capacity(response.body().len().unwrap_or(0).try_into()?);
response.copy_to(&mut bytes).await?;
Ok(Response::builder()
.status(response.status())
.body(bytes.into())?)
}
}
}
pub trait ResponseExt: Sized {
fn error_for_status(self) -> Result<Self, TraceError>;
}
impl<T> ResponseExt for Response<T> {
fn error_for_status(self) -> Result<Self, TraceError> {
if self.status().is_success() {
Ok(self)
} else {
Err(format!("request failed with status {}", self.status()).into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_headers_get() {
let mut carrier = http::HeaderMap::new();
HeaderInjector(&mut carrier).set("headerName", "value".to_string());
assert_eq!(
HeaderExtractor(&carrier).get("HEADERNAME"),
Some("value"),
"case insensitive extraction"
)
}
#[test]
fn http_headers_keys() {
let mut carrier = http::HeaderMap::new();
HeaderInjector(&mut carrier).set("headerName1", "value1".to_string());
HeaderInjector(&mut carrier).set("headerName2", "value2".to_string());
let extractor = HeaderExtractor(&carrier);
let got = extractor.keys();
assert_eq!(got.len(), 2);
assert!(got.contains(&"headername1"));
assert!(got.contains(&"headername2"));
}
}