Skip to main content

fast_down/http/
mod.rs

1//! A backend-agnostic HTTP download layer.
2//!
3//! This module defines a set of small traits — [`HttpClient`],
4//! [`HttpRequestBuilder`], [`HttpResponse`], and [`HttpHeaders`] — that abstract
5//! over any HTTP client implementation (for example the `reqwest` wrapper in
6//! the `reqwest` module). On top of them it provides:
7//!
8//! * [`HttpPuller`]: a [`fast_pull::Puller`] that streams bytes over HTTP, with
9//!   range support and file-identity (resumability) checks.
10//! * [`Prefetch`]: resolves a [`crate::UrlInfo`] for a URL via a prefetch request.
11//! * [`ContentDisposition`]: parses the `Content-Disposition` header for filenames.
12//! * [`manual_redirect`]: RFC 9110-aware `Referer` computation for redirect following.
13//! * [`HttpError`]: the error type produced by this layer.
14//!
15//! Most users do not use these types directly; instead they use
16//! `FastDownPuller` (from the `fast-puller` feature), which wraps
17//! [`HttpPuller`] with a smart-redirecting `reqwest` client.
18
19mod content_disposition;
20pub mod manual_redirect;
21mod prefetch;
22mod puller;
23pub use content_disposition::*;
24pub use manual_redirect::*;
25pub use prefetch::*;
26pub use puller::*;
27
28use crate::url_info::FileId;
29use bytes::Bytes;
30use fast_pull::{ProgressEntry, PullResult, PullerError};
31use std::{borrow::Cow, fmt::Debug, future::Future};
32use url::Url;
33
34/// Abstraction over an HTTP client that can send GET requests with optional byte-range headers.
35pub trait HttpClient: Clone + Send + Sync + Unpin + 'static {
36    /// The request builder this client produces.
37    type RequestBuilder: HttpRequestBuilder;
38    /// Start a GET request, optionally restricted to `range`.
39    fn get(&self, url: Url, range: Option<ProgressEntry>) -> Self::RequestBuilder;
40}
41/// Abstraction over an HTTP request builder that can be sent to produce a response.
42pub trait HttpRequestBuilder {
43    /// The response type produced on success.
44    type Response: HttpResponse;
45    /// The error type for a failed send.
46    type RequestError: std::error::Error + Send + Sync + Unpin;
47    /// Send the request, producing a response or a retryable error.
48    fn send(self) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send;
49}
50/// Abstraction over an HTTP response that provides headers, final URL, and chunked body reading.
51pub trait HttpResponse: Send + Sync + Debug + Unpin {
52    /// The response's header map type.
53    type Headers: HttpHeaders;
54    /// The error type produced while reading a body chunk.
55    type ChunkError: std::error::Error + Send + Sync + Unpin;
56    /// The response headers.
57    fn headers(&self) -> &Self::Headers;
58    /// The URL the response was served from, after redirects.
59    fn url(&self) -> &Url;
60    /// Read the next body chunk, or `None` at end of body.
61    fn chunk(&mut self) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send;
62}
63/// Abstraction over HTTP response headers, providing typed get-by-name access.
64pub trait HttpHeaders {
65    /// The error type for a failed header lookup.
66    type GetHeaderError: std::error::Error + Send + Sync + Unpin;
67    /// Get a header value by name.
68    ///
69    /// # Errors
70    /// Returns an error if the header cannot be retrieved
71    fn get(&self, header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError>;
72}
73
74/// Projected [`HttpClient::RequestBuilder`] type for a given client.
75pub type GetRequestBuilder<Client> = <Client as HttpClient>::RequestBuilder;
76/// Projected [`HttpResponse`] type for a given request builder.
77pub type GetResponse<Client> = <GetRequestBuilder<Client> as HttpRequestBuilder>::Response;
78/// Projected request error type for a given client.
79pub type GetRequestError<Client> = <GetRequestBuilder<Client> as HttpRequestBuilder>::RequestError;
80/// Projected chunk error type for a given client.
81pub type GetChunkError<Client> = <GetResponse<Client> as HttpResponse>::ChunkError;
82/// Projected headers type for a given client.
83pub type GetHeader<Client> = <GetResponse<Client> as HttpResponse>::Headers;
84/// Projected header-get error type for a given client.
85pub type GetHeaderError<Client> = <GetHeader<Client> as HttpHeaders>::GetHeaderError;
86
87/// Errors that can occur during HTTP download operations.
88///
89/// Maps to the various stages of an HTTP request: building, streaming chunks,
90/// detecting mismatched file identity, and irrecoverable failures.
91#[derive(thiserror::Error)]
92pub enum HttpError<Client: HttpClient> {
93    /// The HTTP request failed to send.
94    #[error("HTTP request failed: {0:?}")]
95    Request(GetRequestError<Client>),
96    /// Reading a response body chunk failed.
97    #[error("HTTP chunk read failed: {0:?}\n  response: {1:?}")]
98    Chunk(GetChunkError<Client>, GetResponse<Client>),
99    /// A pull failure the engine must not retry.
100    #[error("irrecoverable pull error")]
101    Irrecoverable,
102    /// The response's file identity did not match the expected one.
103    #[error("body mismatch: expected file {0:?}, got different content\n  response: {1:?}")]
104    MismatchedBody(FileId, GetResponse<Client>),
105}
106
107impl<Client: HttpClient> std::fmt::Debug for HttpError<Client> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::Request(e) => f.debug_tuple("Request").field(e).finish(),
111            Self::Chunk(e, r) => f.debug_tuple("Chunk").field(e).field(r).finish(),
112            Self::Irrecoverable => f.write_str("Irrecoverable"),
113            Self::MismatchedBody(id, r) => {
114                f.debug_tuple("MismatchedBody").field(id).field(r).finish()
115            }
116        }
117    }
118}
119
120impl<C: HttpClient> PullerError for HttpError<C> {
121    fn is_irrecoverable(&self) -> bool {
122        matches!(self, Self::Irrecoverable)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
129    use super::{HttpClient, HttpError, HttpHeaders, HttpRequestBuilder, HttpResponse};
130    use crate::url_info::FileId;
131    use bytes::Bytes;
132    use fast_pull::{PullResult, PullerError};
133    use std::{borrow::Cow, future::Future};
134    use url::Url;
135
136    #[derive(Clone, Debug)]
137    struct MockClient;
138    impl HttpClient for MockClient {
139        type RequestBuilder = MockRequestBuilder;
140        fn get(&self, _url: Url, _range: Option<fast_pull::ProgressEntry>) -> Self::RequestBuilder {
141            MockRequestBuilder
142        }
143    }
144    struct MockRequestBuilder;
145    impl HttpRequestBuilder for MockRequestBuilder {
146        type Response = MockResponse;
147        type RequestError = MockErr;
148        fn send(
149            self,
150        ) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send {
151            std::future::ready(Ok(MockResponse::new()))
152        }
153    }
154    #[derive(Debug)]
155    struct MockResponse {
156        url: Url,
157    }
158    impl MockResponse {
159        fn new() -> Self {
160            Self {
161                url: Url::parse("http://mock-url").unwrap(),
162            }
163        }
164    }
165    impl HttpResponse for MockResponse {
166        type Headers = MockHeaders;
167        type ChunkError = MockErr;
168        fn headers(&self) -> &Self::Headers {
169            &MockHeaders
170        }
171        fn url(&self) -> &Url {
172            &self.url
173        }
174        fn chunk(
175            &mut self,
176        ) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send {
177            std::future::ready(Ok(None))
178        }
179    }
180    #[derive(Debug)]
181    struct MockHeaders;
182    impl HttpHeaders for MockHeaders {
183        type GetHeaderError = MockErr;
184        fn get(&self, _header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError> {
185            Err(MockErr)
186        }
187    }
188    #[derive(Debug, thiserror::Error)]
189    #[error("MockError")]
190    struct MockErr;
191
192    #[test]
193    fn debug_impl_formats_every_variant() {
194        let request = HttpError::<MockClient>::Request(MockErr);
195        assert!(format!("{request:?}").contains("Request"));
196
197        let chunk = HttpError::<MockClient>::Chunk(MockErr, MockResponse::new());
198        assert!(format!("{chunk:?}").contains("Chunk"));
199
200        let irrecoverable = HttpError::<MockClient>::Irrecoverable;
201        assert_eq!(format!("{irrecoverable:?}"), "Irrecoverable");
202
203        let mismatched = HttpError::<MockClient>::MismatchedBody(
204            FileId::new(Some("etag-x"), None),
205            MockResponse::new(),
206        );
207        assert!(format!("{mismatched:?}").contains("MismatchedBody"));
208    }
209
210    #[test]
211    fn is_irrecoverable_only_for_variant() {
212        assert!(HttpError::<MockClient>::Irrecoverable.is_irrecoverable());
213        assert!(!HttpError::<MockClient>::Request(MockErr).is_irrecoverable());
214        assert!(!HttpError::<MockClient>::Chunk(MockErr, MockResponse::new()).is_irrecoverable());
215        assert!(
216            !HttpError::<MockClient>::MismatchedBody(
217                FileId::new(Some("x"), None),
218                MockResponse::new()
219            )
220            .is_irrecoverable()
221        );
222    }
223}