1#![cfg(not(target_family = "wasm"))]
2
3use crate::http::{
18 HttpClient, HttpHeaders, HttpRequestBuilder, HttpResponse,
19 manual_redirect::{ReferrerPolicy, compute_referer},
20};
21use fast_pull::{ProgressEntry, PullResult};
22use httpdate::parse_http_date;
23use reqwest::{
24 Client, RequestBuilder, Response, StatusCode,
25 header::{self, HeaderMap, HeaderValue},
26};
27use std::{
28 borrow::Cow,
29 time::{Duration, SystemTime},
30};
31use url::Url;
32
33mod error;
34pub use error::*;
35
36impl HttpClient for Client {
37 type RequestBuilder = RequestBuilder;
38 fn get(&self, url: Url, range: Option<ProgressEntry>) -> Self::RequestBuilder {
39 let mut req = self.get(url);
40 if let Some(range) = range {
41 req = req.header(
42 header::RANGE,
43 format!("bytes={}-{}", range.start, range.end.saturating_sub(1)),
44 );
45 }
46 req
47 }
48}
49
50impl HttpRequestBuilder for RequestBuilder {
51 type Response = Response;
52 type RequestError = ReqwestResponseError;
53 async fn send(self) -> PullResult<Self::Response, Self::RequestError> {
54 let res = self
55 .send()
56 .await
57 .map_err(|e| (ReqwestResponseError::Request(e.into()), None))?;
58 let status = res.status();
59 if status.is_success() {
60 Ok(res)
61 } else {
62 let retry_after = parse_retry_after(res.headers());
63 Err((ReqwestResponseError::from_response(res), retry_after))
64 }
65 }
66}
67
68impl HttpResponse for Response {
69 type Headers = HeaderMap;
70 type ChunkError = reqwest::Error;
71 fn headers(&self) -> &Self::Headers {
72 self.headers()
73 }
74 fn url(&self) -> &Url {
75 self.url()
76 }
77 async fn chunk(&mut self) -> Result<Option<bytes::Bytes>, Self::ChunkError> {
78 self.chunk().await
79 }
80}
81
82impl HttpHeaders for HeaderMap {
83 type GetHeaderError = ReqwestGetHeaderError;
84 fn get(&self, header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError> {
85 let header_value = self.get(header).ok_or(ReqwestGetHeaderError::NotFound)?;
86 Ok(String::from_utf8_lossy(header_value.as_bytes()))
87 }
88}
89
90#[must_use]
95pub fn parse_retry_after(headers: &HeaderMap) -> Option<Duration> {
96 let retry_after = headers.get(header::RETRY_AFTER)?;
97 let retry_str = retry_after.to_str().ok()?;
98 if let Ok(secs) = retry_str.parse::<u64>() {
99 return Some(Duration::from_secs(secs));
100 }
101 let target_time = parse_http_date(retry_str).ok()?;
102 let duration = target_time.duration_since(SystemTime::now()).ok()?;
103 Some(duration)
104}
105
106#[derive(Debug, Clone)]
109pub struct SmartRedirectClient {
110 client: Client,
111 initial_referer: Option<HeaderValue>,
112 referrer_policy: Option<ReferrerPolicy>,
113 origin: Option<HeaderValue>,
116 authorization: Option<HeaderValue>,
117 cookie: Option<HeaderValue>,
118 max_redirects: usize,
119}
120
121impl SmartRedirectClient {
122 #[must_use]
135 pub const fn new(
136 client: Client,
137 initial_referer: Option<HeaderValue>,
138 referrer_policy: Option<ReferrerPolicy>,
139 origin: Option<HeaderValue>,
140 authorization: Option<HeaderValue>,
141 cookie: Option<HeaderValue>,
142 max_redirects: usize,
143 ) -> Self {
144 Self {
145 client,
146 initial_referer,
147 referrer_policy,
148 origin,
149 authorization,
150 cookie,
151 max_redirects,
152 }
153 }
154}
155
156impl HttpClient for SmartRedirectClient {
157 type RequestBuilder = ManualRedirectRequestBuilder;
158
159 fn get(&self, url: Url, range: Option<ProgressEntry>) -> Self::RequestBuilder {
160 ManualRedirectRequestBuilder {
161 client: self.client.clone(),
162 url,
163 range,
164 next_referer: self.initial_referer.clone(),
165 referrer_policy: self.referrer_policy,
166 origin: self.origin.clone(),
167 authorization: self.authorization.clone(),
168 cookie: self.cookie.clone(),
169 max_redirects: self.max_redirects,
170 redirect_count: 0,
171 }
172 }
173}
174
175pub struct ManualRedirectRequestBuilder {
186 client: Client,
187 url: Url,
188 range: Option<ProgressEntry>,
189 next_referer: Option<HeaderValue>,
190 referrer_policy: Option<ReferrerPolicy>,
191 origin: Option<HeaderValue>,
193 authorization: Option<HeaderValue>,
194 cookie: Option<HeaderValue>,
195 max_redirects: usize,
196 redirect_count: usize,
197}
198
199impl HttpRequestBuilder for ManualRedirectRequestBuilder {
200 type Response = Response;
201 type RequestError = ReqwestResponseError;
202
203 async fn send(mut self) -> PullResult<Response, Self::RequestError> {
204 loop {
205 let mut req = self.client.get(self.url.clone());
206 if let Some(ref range) = self.range {
207 req = req.header(
208 header::RANGE,
209 format!("bytes={}-{}", range.start, range.end.saturating_sub(1)),
210 );
211 }
212 if let Some(ref referer) = self.next_referer {
213 req = req.header(header::REFERER, referer);
214 }
215 if self.redirect_count == 0 {
218 if let Some(ref origin) = self.origin {
219 req = req.header(header::ORIGIN, origin);
220 }
221 if let Some(ref auth) = self.authorization {
222 req = req.header(header::AUTHORIZATION, auth);
223 }
224 if let Some(ref cookie) = self.cookie {
225 req = req.header(header::COOKIE, cookie);
226 }
227 }
228 let resp = req
229 .send()
230 .await
231 .map_err(|e| (ReqwestResponseError::Request(e.into()), None))?;
232
233 debug_assert!(
238 resp.url() == &self.url,
239 "SmartRedirectClient: inner reqwest::Client has auto-redirect ENABLED. \
240 Build it with `.redirect(reqwest::redirect::Policy::none())`. \
241 The Referer-Policy aware redirect logic requires full control over redirects."
242 );
243
244 let status = resp.status();
245 if !is_redirection(status) {
246 return if status.is_success() {
247 Ok(resp)
248 } else {
249 let retry_after = parse_retry_after(resp.headers());
250 Err((ReqwestResponseError::from_response(resp), retry_after))
251 };
252 }
253 if self.redirect_count >= self.max_redirects {
254 let retry_after = parse_retry_after(resp.headers());
255 return Err((ReqwestResponseError::from_response(resp), retry_after));
256 }
257 let location = if let Some(v) = resp.headers().get(header::LOCATION)
258 && let Ok(s) = v.to_str()
259 {
260 s
261 } else {
262 let retry_after = parse_retry_after(resp.headers());
263 return Err((ReqwestResponseError::from_response(resp), retry_after));
264 };
265 let Ok(mut next_url) = self.url.join(location) else {
266 let retry_after = parse_retry_after(resp.headers());
267 return Err((ReqwestResponseError::from_response(resp), retry_after));
268 };
269 if next_url.fragment().is_none()
272 && let Some(fragment) = self.url.fragment()
273 {
274 next_url.set_fragment(Some(fragment));
275 }
276 if let Some(policy_header) = resp.headers().get("referrer-policy")
277 && let Ok(s) = policy_header.to_str()
278 && let Some(p) = ReferrerPolicy::parse(s)
279 {
280 self.referrer_policy = Some(p);
281 }
282 self.next_referer = compute_referer(self.referrer_policy, &self.url, &next_url)
283 .and_then(|s| HeaderValue::from_str(&s).ok());
284 self.url = next_url;
285 self.redirect_count += 1;
286 }
287 }
288}
289
290fn is_redirection(status: StatusCode) -> bool {
291 matches!(
292 status,
293 StatusCode::MOVED_PERMANENTLY
294 | StatusCode::FOUND
295 | StatusCode::SEE_OTHER
296 | StatusCode::TEMPORARY_REDIRECT
297 | StatusCode::PERMANENT_REDIRECT
298 )
299}
300
301#[cfg(test)]
302mod tests {
303 #![allow(
304 clippy::unwrap_used,
305 clippy::expect_used,
306 clippy::panic,
307 clippy::significant_drop_tightening
308 )]
309 use super::*;
310 use crate::{
311 http::{HttpPuller, Prefetch},
312 url_info::FileId,
313 };
314 use fast_pull::{
315 Event, MemPusher, Merge,
316 mock::build_mock_data,
317 multi::{self, download_multi},
318 single::{self, download_single},
319 };
320 use reqwest::{Client, StatusCode};
321 use std::sync::Arc;
322 use std::time::Duration;
323
324 #[tokio::test]
325 async fn test_redirect_and_content_range() {
326 let mut server = mockito::Server::new_async().await;
327 let client = Client::builder().no_proxy().build().unwrap();
328
329 let _mock_redirect = server
330 .mock("GET", "/redirect")
331 .with_status(301)
332 .with_header("Location", "/%e4%bd%a0%e5%a5%bd.txt")
333 .create_async()
334 .await;
335
336 let _mock_file = server
337 .mock("GET", "/%e4%bd%a0%e5%a5%bd.txt")
338 .with_status(206)
339 .with_header("Content-Length", "1024")
340 .with_header("Content-Range", "bytes 0-0/1024")
341 .with_body(vec![0; 1024])
342 .create_async()
343 .await;
344
345 let url = Url::parse(&format!("{}/redirect", server.url())).unwrap();
346 let (url_info, _) = client.prefetch(url).await.expect("Request should succeed");
347
348 assert_eq!(
349 url_info.final_url.as_str(),
350 format!("{}/%e4%bd%a0%e5%a5%bd.txt", server.url())
351 );
352 assert_eq!(url_info.size, 1024);
353 assert_eq!(url_info.raw_name, "你好.txt");
354 assert!(url_info.supports_range);
355 }
356
357 #[tokio::test]
358 async fn test_filename_sources() {
359 let mut server = mockito::Server::new_async().await;
360 let client = Client::builder().no_proxy().build().unwrap();
361
362 let _mock1 = server
364 .mock("GET", "/test1")
365 .with_header("Content-Disposition", r#"attachment; filename="test.txt""#)
366 .create_async()
367 .await;
368 let url = Url::parse(&format!("{}/test1", server.url())).unwrap();
369 let (url_info, _) = client.prefetch(url).await.unwrap();
370 assert_eq!(url_info.raw_name, "test.txt");
371
372 let _mock_star = server
374 .mock("GET", "/test_star")
375 .with_header(
376 "Content-Disposition",
377 "attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt",
378 ) .create_async()
380 .await;
381 let url = Url::parse(&format!("{}/test_star", server.url())).unwrap();
382 let (url_info, _) = client.prefetch(url).await.unwrap();
383 assert_eq!(url_info.raw_name, "测试.txt");
384
385 let _mock_both = server
386 .mock("GET", "/test_both")
387 .with_header(
388 "Content-Disposition",
389 r#"attachment; filename="fallback.txt"; filename*=UTF-8''%E6%B5%8B%E8%AF%95.txt"#,
390 )
391 .create_async()
392 .await;
393 let url = Url::parse(&format!("{}/test_both", server.url())).unwrap();
394 let (url_info, _) = client.prefetch(url).await.unwrap();
395 assert_eq!(url_info.raw_name, "测试.txt");
396
397 let _mock2 = server
399 .mock("GET", "/test2/%E5%A5%BD%E5%A5%BD%E5%A5%BD.pdf")
400 .create_async()
401 .await;
402 let url = Url::parse(&format!(
403 "{}/test2/%E5%A5%BD%E5%A5%BD%E5%A5%BD.pdf",
404 server.url()
405 ))
406 .unwrap();
407 let (url_info, _) = client.prefetch(url).await.unwrap();
408 assert_eq!(url_info.raw_name, "好好好.pdf");
409 }
410
411 #[tokio::test]
412 async fn test_concurrent_download() {
413 let mock_data = build_mock_data(8 * 1024 * 1024);
417 let mut server = mockito::Server::new_async().await;
418 let client = Client::builder().no_proxy().build().unwrap();
419 let _mock = mount_range_endpoint(&mut server, "/concurrent", mock_data.clone()).await;
420 let puller = HttpPuller::new(
421 Arc::new(format!("{}/concurrent", server.url()).parse().unwrap()),
422 client,
423 None,
424 FileId::default(),
425 );
426 let pusher = MemPusher::with_capacity(mock_data.len());
427 let receive = pusher.receive.clone();
432 #[allow(clippy::single_range_in_vec_init)]
433 let download_chunks = vec![0..mock_data.len() as u64];
434 let result = download_multi(
435 puller,
436 pusher,
437 multi::DownloadOptions {
438 concurrent: 32,
439 retry_gap: Duration::from_secs(1),
440 push_queue_cap: 1024,
441 download_chunks: download_chunks.iter().cloned(),
442 pull_timeout: Duration::from_secs(5),
443 min_chunk_size: 1,
444 max_speculative: 3,
445 },
446 );
447
448 let mut pull_progress: Vec<ProgressEntry> = Vec::new();
449 let mut push_progress: Vec<ProgressEntry> = Vec::new();
450 while let Ok(e) = result.event_chain().recv().await {
454 match e {
455 Event::PullProgress(_, p) => pull_progress.merge_progress(p),
456 Event::PushProgress(p) => push_progress.merge_progress(p),
457 _ => {}
458 }
459 }
460 dbg!(&pull_progress);
461 dbg!(&push_progress);
462 assert_eq!(pull_progress, download_chunks);
463 assert_eq!(push_progress, download_chunks);
464
465 assert_eq!(&**receive.lock(), mock_data);
466 }
467
468 async fn mount_range_endpoint(
471 server: &mut mockito::ServerGuard,
472 path: &str,
473 body: Vec<u8>,
474 ) -> mockito::Mock {
475 server
476 .mock("GET", path)
477 .with_status(206)
478 .with_header("Accept-Ranges", "bytes")
479 .with_body_from_request(move |request| {
480 if !request.has_header("Range") {
481 return body.clone();
482 }
483 request.header("Range")[0]
484 .to_str()
485 .unwrap()
486 .rsplit('=')
487 .next()
488 .unwrap()
489 .split(',')
490 .map(|p| p.trim().splitn(2, '-'))
491 .map(|mut p| {
492 let start = p.next().unwrap().parse::<usize>().unwrap();
493 let end = p.next().unwrap().parse::<usize>().unwrap();
494 start..=end
495 })
496 .flat_map(|p| body[p].to_vec())
497 .collect()
498 })
499 .create_async()
500 .await
501 }
502
503 #[cfg(feature = "file")]
509 async fn temp_mmap_target(
510 tag: &str,
511 size: u64,
512 ) -> (fast_pull::MmapFilePusher, std::path::PathBuf) {
513 let dir = std::env::temp_dir().join(format!("fast-down-{tag}-{}", std::process::id()));
514 std::fs::create_dir_all(&dir).unwrap();
515 let path = dir.join("out.bin");
516 let file = tokio::fs::OpenOptions::new()
517 .read(true)
518 .write(true)
519 .create(true)
520 .truncate(true)
521 .open(&path)
522 .await
523 .unwrap();
524 let pusher = fast_pull::MmapFilePusher::new(&file, size, false)
525 .await
526 .unwrap();
527 (pusher, path)
528 }
529
530 #[cfg(feature = "file")]
532 fn next_rand(state: &mut u64) -> u64 {
533 *state ^= *state << 13;
534 *state ^= *state >> 7;
535 *state ^= *state << 17;
536 *state
537 }
538
539 #[cfg(feature = "file")]
547 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
548 async fn test_concurrent_download_thread_churn_preserves_all_bytes() {
549 use std::collections::BTreeSet;
550 use std::sync::atomic::{AtomicU64, Ordering};
551
552 let mock_data = build_mock_data(4 * 1024 * 1024);
553 let mut server = mockito::Server::new_async().await;
554 let client = Client::builder().no_proxy().build().unwrap();
555 let _mock = mount_range_endpoint(&mut server, "/churn", mock_data.clone()).await;
556
557 let size = mock_data.len() as u64;
558 let (pusher, path) = temp_mmap_target("churn", size).await;
559
560 let puller = HttpPuller::new(
561 Arc::new(format!("{}/churn", server.url()).parse().unwrap()),
562 client,
563 None,
564 FileId::default(),
565 );
566 #[allow(clippy::single_range_in_vec_init)]
567 let download_chunks = vec![0..size];
568 let result = download_multi(
571 puller,
572 pusher,
573 multi::DownloadOptions {
574 concurrent: 1,
575 retry_gap: Duration::from_secs(1),
576 push_queue_cap: 1024,
577 download_chunks: download_chunks.iter().cloned(),
578 pull_timeout: Duration::from_secs(30),
579 min_chunk_size: 1,
580 max_speculative: 3,
581 },
582 );
583
584 let written = Arc::new(AtomicU64::new(0));
585 let churner = result.clone();
586 let probe = written.clone();
587 let churn = tokio::spawn(async move {
588 let mut state = 0x2545_F491_4F6C_DD1D_u64;
589 let mut seen = BTreeSet::new();
590 let mut inflight = 0usize;
591 for _ in 0..3000 {
595 let done = probe.load(Ordering::Relaxed);
596 if done >= size {
597 break;
598 }
599 if done > 0 {
600 inflight += 1;
601 }
602 let threads = usize::try_from(next_rand(&mut state) % 8 + 1).unwrap();
603 seen.insert(threads);
604 churner.set_threads(threads);
605 tokio::time::sleep(Duration::from_millis(5)).await;
606 }
607 churner.set_threads(8);
609 (seen, inflight)
610 });
611
612 let mut pulling_total = 0usize;
613 let mut push_progress: Vec<ProgressEntry> = Vec::new();
614 while let Ok(e) = result.event_chain().recv().await {
615 match e {
616 Event::Pulling(_) => pulling_total += 1,
617 Event::PushProgress(p) => {
618 written.fetch_add(p.end - p.start, Ordering::Relaxed);
619 push_progress.merge_progress(p);
620 }
621 _ => {}
622 }
623 }
624 let (seen, inflight) = churn.await.unwrap();
625
626 assert!(
629 seen.len() > 2,
630 "churn never varied the pool size, so nothing was exercised: {seen:?}"
631 );
632 assert!(
633 inflight > 0,
634 "every resize landed outside the transfer, so churn was a no-op"
635 );
636 assert!(
641 pulling_total >= 8,
642 "ranges were never redistributed across workers (pulling={pulling_total}, chunks={})",
643 download_chunks.len()
644 );
645 assert_eq!(
646 push_progress, download_chunks,
647 "repeated resizing lost or duplicated pushed ranges"
648 );
649
650 let on_disk = tokio::fs::read(&path).await.unwrap();
653 assert!(
654 on_disk == mock_data,
655 "repeated resizing corrupted the bytes written to disk"
656 );
657 let _ = std::fs::remove_dir_all(path.parent().unwrap());
658 }
659
660 #[tokio::test]
661 async fn test_sequential_download() {
662 let mock_data = build_mock_data(8 * 1024 * 1024);
665 let mut server = mockito::Server::new_async().await;
666 let client = Client::builder().no_proxy().build().unwrap();
667 let _mock = server
668 .mock("GET", "/sequential")
669 .with_status(200)
670 .with_body(mock_data.clone())
671 .create_async()
672 .await;
673 let puller = HttpPuller::new(
674 Arc::new(format!("{}/sequential", server.url()).parse().unwrap()),
675 client,
676 None,
677 FileId::default(),
678 );
679 let pusher = MemPusher::with_capacity(mock_data.len());
680 let receive = pusher.receive.clone();
685 #[allow(clippy::single_range_in_vec_init)]
686 let download_chunks = vec![0..mock_data.len() as u64];
687 let result = download_single(
688 puller,
689 pusher,
690 single::DownloadOptions {
691 retry_gap: Duration::from_secs(1),
692 push_queue_cap: 1024,
693 },
694 );
695
696 let mut pull_progress: Vec<ProgressEntry> = Vec::new();
697 let mut push_progress: Vec<ProgressEntry> = Vec::new();
698 while let Ok(e) = result.event_chain().recv().await {
702 match e {
703 Event::PullProgress(_, p) => pull_progress.merge_progress(p),
704 Event::PushProgress(p) => push_progress.merge_progress(p),
705 _ => {}
706 }
707 }
708 dbg!(&pull_progress);
709 dbg!(&push_progress);
710 assert_eq!(pull_progress, download_chunks);
711 assert_eq!(push_progress, download_chunks);
712
713 assert_eq!(&**receive.lock(), mock_data);
714 }
715
716 #[tokio::test]
722 async fn test_smart_redirect_follows_redirect() {
723 let mut server = mockito::Server::new_async().await;
724 let client = Client::builder()
728 .no_proxy()
729 .redirect(reqwest::redirect::Policy::none())
730 .build()
731 .unwrap();
732
733 let _mock_redirect = server
734 .mock("GET", "/src")
735 .with_status(302)
736 .with_header("Location", "/dst")
737 .create_async()
738 .await;
739 let _mock_dst = server
740 .mock("GET", "/dst")
741 .with_status(200)
742 .with_header("Content-Length", "11")
743 .with_body("hello world")
744 .create_async()
745 .await;
746
747 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
748 let url = Url::parse(&format!("{}/src", server.url())).unwrap();
749 let resp = redirect_client
750 .get(url, None)
751 .send()
752 .await
753 .expect("manual redirect should succeed");
754 assert_eq!(resp.status(), StatusCode::OK);
755 assert_eq!(resp.url().path(), "/dst");
758 }
759
760 #[tokio::test]
764 async fn test_smart_redirect_respects_max_redirects() {
765 let mut server = mockito::Server::new_async().await;
766 let client = Client::builder()
767 .no_proxy()
768 .redirect(reqwest::redirect::Policy::none())
769 .build()
770 .unwrap();
771 let _mock_loop = server
772 .mock("GET", "/loop")
773 .with_status(301)
774 .with_header("Location", "/loop")
775 .create_async()
776 .await;
777
778 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 2);
779 let url = Url::parse(&format!("{}/loop", server.url())).unwrap();
780 let err = redirect_client
781 .get(url, None)
782 .send()
783 .await
784 .expect_err("exceeding max_redirects should fail");
785 assert!(matches!(err.0, ReqwestResponseError::StatusCode(_)));
786 }
787
788 #[test]
789 fn parse_retry_after_missing_is_none() {
790 let headers = HeaderMap::new();
791 assert_eq!(parse_retry_after(&headers), None);
792 }
793
794 #[test]
795 fn parse_retry_after_delta_seconds() {
796 let mut headers = HeaderMap::new();
797 headers.insert(header::RETRY_AFTER, HeaderValue::from_static("120"));
798 assert_eq!(parse_retry_after(&headers), Some(Duration::from_mins(2)));
799 }
800
801 #[test]
802 fn parse_retry_after_http_date() {
803 let mut headers = HeaderMap::new();
804 let future = SystemTime::now() + Duration::from_hours(1);
807 let date_str = httpdate::fmt_http_date(future);
808 headers.insert(
809 header::RETRY_AFTER,
810 HeaderValue::from_str(&date_str).unwrap(),
811 );
812 let delay = parse_retry_after(&headers);
813 assert!(delay.is_some());
814 assert!(delay.unwrap() > Duration::ZERO);
815 }
816
817 #[test]
818 fn parse_retry_after_unparseable_is_none() {
819 let mut headers = HeaderMap::new();
821 headers.insert(header::RETRY_AFTER, HeaderValue::from_static("not-a-time"));
822 assert_eq!(parse_retry_after(&headers), None);
823 }
824
825 #[test]
826 fn parse_retry_after_past_date_is_none() {
827 let mut headers = HeaderMap::new();
829 headers.insert(
830 header::RETRY_AFTER,
831 HeaderValue::from_static("Mon, 01 Jan 2001 00:00:00 GMT"),
832 );
833 assert_eq!(parse_retry_after(&headers), None);
834 }
835
836 #[tokio::test]
837 async fn test_smart_redirect_404_returns_status_code_error() {
838 let mut server = mockito::Server::new_async().await;
839 let client = Client::builder()
840 .no_proxy()
841 .redirect(reqwest::redirect::Policy::none())
842 .build()
843 .unwrap();
844 let _mock = server
845 .mock("GET", "/404")
846 .with_status(404)
847 .create_async()
848 .await;
849 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
850 let url = Url::parse(&format!("{}/404", server.url())).unwrap();
851 let err = redirect_client
852 .get(url, None)
853 .send()
854 .await
855 .expect_err("404 should produce a StatusCode error");
856 assert!(matches!(err.0, ReqwestResponseError::StatusCode(_)));
857 }
858
859 #[tokio::test]
860 async fn test_smart_redirect_302_without_location_errors() {
861 let mut server = mockito::Server::new_async().await;
862 let client = Client::builder()
863 .no_proxy()
864 .redirect(reqwest::redirect::Policy::none())
865 .build()
866 .unwrap();
867 let _mock = server
868 .mock("GET", "/redir")
869 .with_status(302)
870 .create_async()
871 .await;
872 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
873 let url = Url::parse(&format!("{}/redir", server.url())).unwrap();
874 let err = redirect_client
875 .get(url, None)
876 .send()
877 .await
878 .expect_err("a 302 without Location should error");
879 assert!(matches!(err.0, ReqwestResponseError::StatusCode(_)));
880 }
881
882 #[tokio::test]
883 async fn test_smart_redirect_302_unjoinable_location_errors() {
884 let mut server = mockito::Server::new_async().await;
885 let client = Client::builder()
886 .no_proxy()
887 .redirect(reqwest::redirect::Policy::none())
888 .build()
889 .unwrap();
890 let _mock = server
891 .mock("GET", "/redir")
892 .with_status(302)
893 .with_header("Location", "http://")
894 .create_async()
895 .await;
896 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
897 let url = Url::parse(&format!("{}/redir", server.url())).unwrap();
898 let err = redirect_client
899 .get(url, None)
900 .send()
901 .await
902 .expect_err("an unjoinable Location should error");
903 assert!(matches!(err.0, ReqwestResponseError::StatusCode(_)));
904 }
905
906 #[tokio::test]
907 async fn test_smart_redirect_reads_referrer_policy() {
908 let mut server = mockito::Server::new_async().await;
909 let client = Client::builder()
910 .no_proxy()
911 .redirect(reqwest::redirect::Policy::none())
912 .build()
913 .unwrap();
914 let _mock_redir = server
915 .mock("GET", "/src")
916 .with_status(302)
917 .with_header("Location", "/dst")
918 .with_header("Referrer-Policy", "no-referrer")
919 .create_async()
920 .await;
921 let _mock_dst = server
922 .mock("GET", "/dst")
923 .with_status(200)
924 .with_header("Content-Length", "5")
925 .with_body("hello")
926 .create_async()
927 .await;
928 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
929 let url = Url::parse(&format!("{}/src", server.url())).unwrap();
930 let resp = redirect_client
931 .get(url, None)
932 .send()
933 .await
934 .expect("redirect with Referrer-Policy should succeed");
935 assert_eq!(resp.status(), StatusCode::OK);
936 }
937
938 #[tokio::test]
939 async fn test_smart_redirect_injects_resource_headers() {
940 let mut server = mockito::Server::new_async().await;
941 let client = Client::builder()
942 .no_proxy()
943 .redirect(reqwest::redirect::Policy::none())
944 .build()
945 .unwrap();
946 let _mock = server
947 .mock("GET", "/dst")
948 .with_status(200)
949 .with_header("Content-Length", "5")
950 .with_body("hello")
951 .create_async()
952 .await;
953 let redirect_client = SmartRedirectClient::new(
955 client,
956 None,
957 Some(ReferrerPolicy::NoReferrer),
958 Some(HeaderValue::from_static("https://origin.example")),
959 Some(HeaderValue::from_static("secret-token")),
960 Some(HeaderValue::from_static("cookie-value")),
961 10,
962 );
963 let url = Url::parse(&format!("{}/dst", server.url())).unwrap();
964 let resp = redirect_client
965 .get(url, None)
966 .send()
967 .await
968 .expect("request with resource headers should succeed");
969 assert_eq!(resp.status(), StatusCode::OK);
970 }
971
972 #[tokio::test]
973 async fn test_smart_redirect_with_range_request() {
974 let mut server = mockito::Server::new_async().await;
975 let client = Client::builder()
976 .no_proxy()
977 .redirect(reqwest::redirect::Policy::none())
978 .build()
979 .unwrap();
980 let _mock = server
981 .mock("GET", "/range")
982 .with_status(200)
983 .with_header("Content-Length", "4")
984 .with_body("data")
985 .create_async()
986 .await;
987 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
988 let url = Url::parse(&format!("{}/range", server.url())).unwrap();
989 let resp = redirect_client
992 .get(url, Some(0..3))
993 .send()
994 .await
995 .expect("ranged request must succeed");
996 assert_eq!(resp.status(), StatusCode::OK);
997 }
998
999 #[tokio::test]
1003 async fn test_smart_redirect_preserves_range_across_redirect() {
1004 let mut server = mockito::Server::new_async().await;
1005 let client = Client::builder()
1006 .no_proxy()
1007 .redirect(reqwest::redirect::Policy::none())
1008 .build()
1009 .unwrap();
1010 let _mock_src = server
1011 .mock("GET", "/src")
1012 .with_status(302)
1013 .with_header("Location", "/dst")
1014 .create_async()
1015 .await;
1016 let _mock_dst = server
1018 .mock("GET", "/dst")
1019 .match_header("Range", "bytes=0-2")
1020 .with_status(206)
1021 .with_header("Content-Length", "3")
1022 .with_body("dat")
1023 .create_async()
1024 .await;
1025 let redirect_client = SmartRedirectClient::new(client, None, None, None, None, None, 10);
1026 let url = Url::parse(&format!("{}/src", server.url())).unwrap();
1027 let resp = redirect_client
1029 .get(url, Some(0..3))
1030 .send()
1031 .await
1032 .expect("ranged request across redirect must succeed");
1033 assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
1034 }
1035}
1036
1037#[cfg(test)]
1038mod range_underflow_tests {
1039 #![allow(clippy::unwrap_used, clippy::expect_used)]
1040 use super::*;
1041 use crate::http::HttpClient;
1042
1043 #[test]
1044 fn client_get_empty_range_does_not_underflow() {
1045 let client = reqwest::Client::builder().no_proxy().build().unwrap();
1049 let _ = HttpClient::get(
1050 &client,
1051 Url::parse("http://example.com/x").unwrap(),
1052 Some(0..0u64),
1053 );
1054 }
1055
1056 #[tokio::test]
1057 async fn smart_redirect_get_empty_range_does_not_underflow() {
1058 let mut server = mockito::Server::new_async().await;
1062 let client = reqwest::Client::builder()
1063 .no_proxy()
1064 .redirect(reqwest::redirect::Policy::none())
1065 .build()
1066 .unwrap();
1067 let _mock = server
1068 .mock("GET", "/x")
1069 .with_status(200)
1070 .with_header("Content-Length", "1")
1071 .with_body("a")
1072 .create_async()
1073 .await;
1074 let rc = SmartRedirectClient::new(client, None, None, None, None, None, 10);
1075 let resp = rc
1076 .get(
1077 Url::parse(&format!("{}/x", server.url())).unwrap(),
1078 Some(0..0u64),
1079 )
1080 .send()
1081 .await;
1082 drop(server);
1084 assert!(resp.is_ok(), "empty-range request must not panic");
1085 }
1086}