Skip to main content

fast_down_api/core/
prefetch.rs

1use crate::{Config, Event, PlanError, Tx, utils::build_header};
2use fast_down::{UrlInfo, fast_puller::build_client, http::Prefetch};
3use reqwest::Response;
4use url::Url;
5
6/// Resolve the remote file's metadata, retrying up to
7/// [`Config::retry_times`](crate::Config::retry_times).
8///
9/// On success emits [`Event::Prefetch`] and hands back the [`UrlInfo`] together
10/// with the open [`Response`], which the download pipeline reuses to seed the
11/// first range request instead of paying for another round-trip.
12///
13/// # Errors
14///
15/// Retries that still have an attempt left are reported as
16/// [`Event::PrefetchError`]; the attempt that exhausts the budget is returned
17/// as [`PlanError::Prefetch`] instead, because
18/// [`ReqwestResponseError`](fast_down::reqwest::ReqwestResponseError) cannot be
19/// cloned into both places. A client that cannot be built at all is returned as
20/// [`PlanError::BuildClient`] without emitting an event.
21#[allow(clippy::result_large_err)]
22pub async fn prefetch(
23    url: &Url,
24    config: &Config,
25    tx: &Tx,
26) -> Result<(UrlInfo, Response), PlanError> {
27    let client = build_client(
28        build_header(&config.headers),
29        config.proxy.as_deref(),
30        config.accept_invalid_certs,
31        config.accept_invalid_hostnames,
32        config.cookie_store,
33        config.local_address.first().copied(),
34        config.max_redirects,
35    )
36    .map_err(PlanError::BuildClient)?;
37    let mut retry_count = 0;
38    loop {
39        match client.prefetch(url.clone()).await {
40            Ok(t) => {
41                let _ = tx.send(Event::Prefetch(t.0.clone()));
42                break Ok(t);
43            }
44            Err((e, t)) => {
45                retry_count += 1;
46                if retry_count >= config.retry_times {
47                    return Err(PlanError::Prefetch(e));
48                }
49                let _ = tx.send(Event::PrefetchError(e));
50                tokio::time::sleep(t.unwrap_or(config.retry_gap)).await;
51            }
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    #![allow(clippy::unwrap_used)]
59    use super::*;
60    use crate::create_channel;
61    use std::time::Duration;
62
63    use bytes::Bytes;
64    use http_body_util::Full;
65    use hyper::body::Incoming;
66    use hyper::header::{
67        ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG, LAST_MODIFIED, RANGE,
68    };
69    use hyper::server::conn::http1;
70    use hyper::service::service_fn;
71    use hyper::{Request, Response, StatusCode};
72    use hyper_util::rt::TokioIo;
73    use std::convert::Infallible;
74    use std::sync::Arc;
75
76    #[tokio::test]
77    async fn prefetch_gives_up_after_retries_on_unreachable() {
78        // Exercises the Err branch of prefetch: a connection that is refused
79        // must emit `Event::PrefetchError` for every attempt that still has a
80        // retry left, then return the final failure instead of emitting it.
81        let url = Url::parse("http://127.0.0.1:1/never").unwrap();
82        let config = Config {
83            retry_times: 2,
84            retry_gap: Duration::ZERO,
85            ..Default::default()
86        };
87        let (tx, rx) = create_channel();
88        let result = prefetch(&url, &config, &tx).await;
89        assert!(
90            matches!(result, Err(PlanError::Prefetch(_))),
91            "prefetch must return the last error after exhausting retries"
92        );
93        drop(tx);
94        let mut errors = 0;
95        while let Ok(e) = rx.recv().await {
96            if matches!(e, Event::PrefetchError(_)) {
97                errors += 1;
98            }
99        }
100        assert_eq!(
101            errors, 1,
102            "2 attempts means 1 retryable failure is reported as an event and 1 is returned"
103        );
104    }
105
106    #[tokio::test]
107    #[allow(clippy::too_many_lines)]
108    async fn prefetch_succeeds_against_local_server() {
109        // Exercises the Ok branch of `prefetch` (prefetch.rs lines 19-22): a server
110        // that answers a normal GET with 200 + content-length/etag/last-modified and
111        // a Range probe with 206 + content-range must make `prefetch` resolve to
112        // `Some((UrlInfo, _))`, populate the UrlInfo correctly, and emit
113        // `Event::Prefetch`. The existing test only covers the failure path.
114        let body: Arc<Vec<u8>> = Arc::new(vec![0xABu8; 1024]);
115        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
116        let addr = listener.local_addr().unwrap();
117
118        tokio::spawn(async move {
119            loop {
120                let (stream, _) = listener.accept().await.unwrap();
121                let stream = stream;
122                let body = body.clone();
123                let io = TokioIo::new(stream);
124                tokio::spawn(async move {
125                    let service = service_fn(move |req: Request<Incoming>| {
126                        let body = body.clone();
127                        async move {
128                            let total = body.len();
129                            let is_range = req.headers().contains_key(RANGE);
130                            let body_bytes: Bytes = if is_range {
131                                Bytes::from(vec![0xABu8])
132                            } else {
133                                Bytes::from(body.to_vec())
134                            };
135                            let builder = Response::builder()
136                                .status(if is_range {
137                                    StatusCode::PARTIAL_CONTENT
138                                } else {
139                                    StatusCode::OK
140                                })
141                                .header(
142                                    CONTENT_LENGTH,
143                                    if is_range {
144                                        "1".to_string()
145                                    } else {
146                                        total.to_string()
147                                    },
148                                )
149                                .header(ACCEPT_RANGES, "bytes")
150                                .header(ETAG, "etag-test")
151                                .header(LAST_MODIFIED, "Wed, 21 Oct 2026 07:28:00 GMT")
152                                .header(CONTENT_TYPE, "application/octet-stream");
153                            let builder = if is_range {
154                                builder.header(CONTENT_RANGE, format!("bytes 0-0/{total}"))
155                            } else {
156                                builder
157                            };
158                            let resp = builder.body(Full::new(body_bytes)).unwrap();
159                            Ok::<_, Infallible>(resp)
160                        }
161                    });
162                    let _ = http1::Builder::new().serve_connection(io, service).await;
163                });
164            }
165        });
166
167        let url = Url::parse(&format!("http://{addr}/file.bin")).unwrap();
168        let config = Config::default();
169        let (tx, rx) = create_channel();
170        let result = prefetch(&url, &config, &tx).await;
171
172        assert!(
173            result.is_ok(),
174            "prefetch must succeed against a well-behaved server"
175        );
176        let (info, _resp) = result.unwrap();
177        assert_eq!(info.size, 1024, "size must come from content-length");
178        assert_eq!(
179            info.file_id.etag.as_deref(),
180            Some("etag-test"),
181            "etag must be read from the response"
182        );
183        assert!(
184            info.supports_range,
185            "a 206 content-range response must advertise range support"
186        );
187        assert!(
188            info.fast_download,
189            "size > 0 with range support must enable fast_download"
190        );
191
192        drop(tx);
193        let mut saw_prefetch = false;
194        while let Ok(e) = rx.recv().await {
195            if matches!(e, Event::Prefetch(_)) {
196                saw_prefetch = true;
197            }
198        }
199        assert!(
200            saw_prefetch,
201            "prefetch must emit Event::Prefetch on the success path"
202        );
203    }
204}