1mod 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
34pub trait HttpClient: Clone + Send + Sync + Unpin + 'static {
36 type RequestBuilder: HttpRequestBuilder;
38 fn get(&self, url: Url, range: Option<ProgressEntry>) -> Self::RequestBuilder;
40}
41pub trait HttpRequestBuilder {
43 type Response: HttpResponse;
45 type RequestError: std::error::Error + Send + Sync + Unpin;
47 fn send(self) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send;
49}
50pub trait HttpResponse: Send + Sync + Debug + Unpin {
52 type Headers: HttpHeaders;
54 type ChunkError: std::error::Error + Send + Sync + Unpin;
56 fn headers(&self) -> &Self::Headers;
58 fn url(&self) -> &Url;
60 fn chunk(&mut self) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send;
62}
63pub trait HttpHeaders {
65 type GetHeaderError: std::error::Error + Send + Sync + Unpin;
67 fn get(&self, header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError>;
72}
73
74pub type GetRequestBuilder<Client> = <Client as HttpClient>::RequestBuilder;
76pub type GetResponse<Client> = <GetRequestBuilder<Client> as HttpRequestBuilder>::Response;
78pub type GetRequestError<Client> = <GetRequestBuilder<Client> as HttpRequestBuilder>::RequestError;
80pub type GetChunkError<Client> = <GetResponse<Client> as HttpResponse>::ChunkError;
82pub type GetHeader<Client> = <GetResponse<Client> as HttpResponse>::Headers;
84pub type GetHeaderError<Client> = <GetHeader<Client> as HttpHeaders>::GetHeaderError;
86
87#[derive(thiserror::Error)]
92pub enum HttpError<Client: HttpClient> {
93 #[error("HTTP request failed: {0:?}")]
95 Request(GetRequestError<Client>),
96 #[error("HTTP chunk read failed: {0:?}\n response: {1:?}")]
98 Chunk(GetChunkError<Client>, GetResponse<Client>),
99 #[error("irrecoverable pull error")]
101 Irrecoverable,
102 #[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}