use crate::{Error, Result};
use async_trait::async_trait;
use futures_core::Stream;
use std::pin::Pin;
use std::time::Duration;
pub type ByteStream = Pin<Box<dyn Stream<Item = Result<Vec<u8>>> + Send + 'static>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
Get,
Post,
}
impl Method {
pub fn as_str(self) -> &'static str {
match self {
Method::Get => "GET",
Method::Post => "POST",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HttpRequest {
pub method: Method,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl HttpRequest {
pub fn new(url: impl Into<String>, body: Vec<u8>) -> Self {
Self {
method: Method::Post,
url: url.into(),
headers: Vec::new(),
body,
}
}
pub fn get(url: impl Into<String>) -> Self {
Self {
method: Method::Get,
url: url.into(),
headers: Vec::new(),
body: Vec::new(),
}
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct HttpResponse {
pub status: u16,
pub body: Vec<u8>,
pub retry_after: Option<Duration>,
}
impl HttpResponse {
pub fn new(status: u16, body: Vec<u8>) -> Self {
Self {
status,
body,
retry_after: None,
}
}
#[must_use]
pub fn with_retry_after(mut self, wait: Duration) -> Self {
self.retry_after = Some(wait);
self
}
pub fn check(&self) -> Result<()> {
let body = String::from_utf8_lossy(&self.body);
let detail = body.chars().take(400).collect::<String>();
match self.status {
200..=299 => Ok(()),
401 | 403 => Err(Error::Auth(detail)),
404 => Err(Error::NotFound(detail)),
408 => Err(Error::Timeout {
elapsed: Duration::ZERO,
}),
429 => Err(Error::RateLimited {
retry_after: self.retry_after,
}),
400 | 422 => Err(Error::InvalidRequest(detail)),
500..=599 => Err(Error::Transient(format!("{}: {detail}", self.status))),
other => Err(Error::Transient(format!("{other}: {detail}"))),
}
}
}
#[async_trait]
pub trait HttpTransport: Send + Sync {
async fn send(&self, request: HttpRequest) -> Result<HttpResponse>;
async fn send_streaming(&self, request: HttpRequest) -> Result<ByteStream> {
let response = self.send(request).await?;
response.check()?;
Ok(Box::pin(Whole {
chunk: Some(response.body),
}))
}
}
struct Whole {
chunk: Option<Vec<u8>>,
}
impl Stream for Whole {
type Item = Result<Vec<u8>>;
fn poll_next(
mut self: Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
std::task::Poll::Ready(self.chunk.take().map(Ok))
}
}
#[cfg(feature = "reqwest")]
#[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
#[derive(Debug, Clone)]
pub struct Reqwest {
client: reqwest::Client,
}
#[cfg(feature = "reqwest")]
impl Reqwest {
pub fn new(timeout: Duration) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.map_err(|e| Error::Transient(format!("http client: {e}")))?;
Ok(Self { client })
}
pub fn with_client(client: reqwest::Client) -> Self {
Self { client }
}
}
#[cfg(feature = "reqwest")]
#[async_trait]
impl HttpTransport for Reqwest {
async fn send(&self, request: HttpRequest) -> Result<HttpResponse> {
let mut builder = match request.method {
Method::Get => self.client.get(&request.url),
Method::Post => self.client.post(&request.url).body(request.body),
};
for (name, value) in &request.headers {
builder = builder.header(name, value);
}
let response = builder.send().await.map_err(|e| {
if e.is_timeout() {
Error::Timeout {
elapsed: Duration::ZERO,
}
} else {
Error::Transient(e.to_string())
}
})?;
let status = response.status().as_u16();
let retry_after = response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.trim().parse::<u64>().ok())
.map(Duration::from_secs);
let body = response
.bytes()
.await
.map_err(|e| Error::Transient(format!("reading the body: {e}")))?
.to_vec();
Ok(HttpResponse {
status,
body,
retry_after,
})
}
async fn send_streaming(&self, request: HttpRequest) -> Result<ByteStream> {
let mut builder = match request.method {
Method::Get => self.client.get(&request.url),
Method::Post => self.client.post(&request.url).body(request.body),
};
for (name, value) in &request.headers {
builder = builder.header(name, value);
}
let response = builder.send().await.map_err(|e| {
if e.is_timeout() {
Error::Timeout {
elapsed: Duration::ZERO,
}
} else {
Error::Transient(e.to_string())
}
})?;
let status = response.status().as_u16();
if !(200..=299).contains(&status) {
let retry_after = response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.trim().parse::<u64>().ok())
.map(Duration::from_secs);
let body = response
.bytes()
.await
.map(|b| b.to_vec())
.unwrap_or_default();
return Err(
match (HttpResponse {
status,
body,
retry_after,
})
.check()
{
Err(e) => e,
Ok(()) => Error::Transient(format!("{status}: not a success and not an error")),
},
);
}
Ok(Box::pin(Chunks {
inner: Box::pin(response.bytes_stream()),
}))
}
}
#[cfg(feature = "reqwest")]
struct Chunks<S> {
inner: Pin<Box<S>>,
}
#[cfg(feature = "reqwest")]
impl<S, B> Stream for Chunks<S>
where
S: Stream<Item = reqwest::Result<B>>,
B: AsRef<[u8]>,
{
type Item = Result<Vec<u8>>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.get_mut().inner.as_mut().poll_next(cx).map(|chunk| {
chunk.map(|chunk| {
chunk
.map(|bytes| bytes.as_ref().to_vec())
.map_err(|e| Error::Transient(format!("reading the stream: {e}")))
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reply(status: u16) -> HttpResponse {
HttpResponse {
status,
body: b"something went wrong".to_vec(),
retry_after: None,
}
}
#[test]
fn a_bad_credential_is_never_retried() {
assert!(!reply(401)
.check()
.err()
.map(|e| e.is_retryable())
.unwrap_or(true));
}
#[test]
fn a_malformed_request_is_not_retried_either() {
let err = reply(400).check().err();
assert!(matches!(err, Some(Error::InvalidRequest(_))));
}
#[test]
fn a_server_fault_is_worth_another_go() {
assert_eq!(
reply(503).check().err().map(|e| e.is_retryable()),
Some(true)
);
}
#[test]
fn a_rate_limit_carries_the_wait_the_server_asked_for() {
let limited = HttpResponse {
status: 429,
body: Vec::new(),
retry_after: Some(Duration::from_secs(12)),
};
assert_eq!(
limited.check().err().and_then(|e| e.retry_after()),
Some(Duration::from_secs(12))
);
}
#[test]
fn a_success_is_not_an_error() {
assert!(reply(200).check().is_ok());
assert!(reply(204).check().is_ok());
}
}