mod content_disposition;
pub mod manual_redirect;
mod prefetch;
mod puller;
pub use content_disposition::*;
pub use manual_redirect::*;
pub use prefetch::*;
pub use puller::*;
use crate::url_info::FileId;
use bytes::Bytes;
use fast_pull::{ProgressEntry, PullResult, PullerError};
use std::{borrow::Cow, fmt::Debug, future::Future};
use url::Url;
pub trait HttpClient: Clone + Send + Sync + Unpin + 'static {
type RequestBuilder: HttpRequestBuilder;
fn get(&self, url: Url, range: Option<ProgressEntry>) -> Self::RequestBuilder;
}
pub trait HttpRequestBuilder {
type Response: HttpResponse;
type RequestError: std::error::Error + Send + Sync + Unpin;
fn send(self) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send;
}
pub trait HttpResponse: Send + Sync + Debug + Unpin {
type Headers: HttpHeaders;
type ChunkError: std::error::Error + Send + Sync + Unpin;
fn headers(&self) -> &Self::Headers;
fn url(&self) -> &Url;
fn chunk(&mut self) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send;
}
pub trait HttpHeaders {
type GetHeaderError: std::error::Error + Send + Sync + Unpin;
fn get(&self, header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError>;
}
pub type GetRequestBuilder<Client> = <Client as HttpClient>::RequestBuilder;
pub type GetResponse<Client> = <GetRequestBuilder<Client> as HttpRequestBuilder>::Response;
pub type GetRequestError<Client> = <GetRequestBuilder<Client> as HttpRequestBuilder>::RequestError;
pub type GetChunkError<Client> = <GetResponse<Client> as HttpResponse>::ChunkError;
pub type GetHeader<Client> = <GetResponse<Client> as HttpResponse>::Headers;
pub type GetHeaderError<Client> = <GetHeader<Client> as HttpHeaders>::GetHeaderError;
#[derive(thiserror::Error)]
pub enum HttpError<Client: HttpClient> {
#[error("HTTP request failed: {0:?}")]
Request(GetRequestError<Client>),
#[error("HTTP chunk read failed: {0:?}\n response: {1:?}")]
Chunk(GetChunkError<Client>, GetResponse<Client>),
#[error("irrecoverable pull error")]
Irrecoverable,
#[error("body mismatch: expected file {0:?}, got different content\n response: {1:?}")]
MismatchedBody(FileId, GetResponse<Client>),
}
impl<Client: HttpClient> std::fmt::Debug for HttpError<Client> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Request(e) => f.debug_tuple("Request").field(e).finish(),
Self::Chunk(e, r) => f.debug_tuple("Chunk").field(e).field(r).finish(),
Self::Irrecoverable => f.write_str("Irrecoverable"),
Self::MismatchedBody(id, r) => {
f.debug_tuple("MismatchedBody").field(id).field(r).finish()
}
}
}
}
impl<C: HttpClient> PullerError for HttpError<C> {
fn is_irrecoverable(&self) -> bool {
matches!(self, Self::Irrecoverable)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::{HttpClient, HttpError, HttpHeaders, HttpRequestBuilder, HttpResponse};
use crate::url_info::FileId;
use bytes::Bytes;
use fast_pull::{PullResult, PullerError};
use std::{borrow::Cow, future::Future};
use url::Url;
#[derive(Clone, Debug)]
struct MockClient;
impl HttpClient for MockClient {
type RequestBuilder = MockRequestBuilder;
fn get(&self, _url: Url, _range: Option<fast_pull::ProgressEntry>) -> Self::RequestBuilder {
MockRequestBuilder
}
}
struct MockRequestBuilder;
impl HttpRequestBuilder for MockRequestBuilder {
type Response = MockResponse;
type RequestError = MockErr;
fn send(
self,
) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send {
std::future::ready(Ok(MockResponse::new()))
}
}
#[derive(Debug)]
struct MockResponse {
url: Url,
}
impl MockResponse {
fn new() -> Self {
Self {
url: Url::parse("http://mock-url").unwrap(),
}
}
}
impl HttpResponse for MockResponse {
type Headers = MockHeaders;
type ChunkError = MockErr;
fn headers(&self) -> &Self::Headers {
&MockHeaders
}
fn url(&self) -> &Url {
&self.url
}
fn chunk(
&mut self,
) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send {
std::future::ready(Ok(None))
}
}
#[derive(Debug)]
struct MockHeaders;
impl HttpHeaders for MockHeaders {
type GetHeaderError = MockErr;
fn get(&self, _header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError> {
Err(MockErr)
}
}
#[derive(Debug, thiserror::Error)]
#[error("MockError")]
struct MockErr;
#[test]
fn debug_impl_formats_every_variant() {
let request = HttpError::<MockClient>::Request(MockErr);
assert!(format!("{request:?}").contains("Request"));
let chunk = HttpError::<MockClient>::Chunk(MockErr, MockResponse::new());
assert!(format!("{chunk:?}").contains("Chunk"));
let irrecoverable = HttpError::<MockClient>::Irrecoverable;
assert_eq!(format!("{irrecoverable:?}"), "Irrecoverable");
let mismatched = HttpError::<MockClient>::MismatchedBody(
FileId::new(Some("etag-x"), None),
MockResponse::new(),
);
assert!(format!("{mismatched:?}").contains("MismatchedBody"));
}
#[test]
fn is_irrecoverable_only_for_variant() {
assert!(HttpError::<MockClient>::Irrecoverable.is_irrecoverable());
assert!(!HttpError::<MockClient>::Request(MockErr).is_irrecoverable());
assert!(!HttpError::<MockClient>::Chunk(MockErr, MockResponse::new()).is_irrecoverable());
assert!(
!HttpError::<MockClient>::MismatchedBody(
FileId::new(Some("x"), None),
MockResponse::new()
)
.is_irrecoverable()
);
}
}