I have constructed a proof-of-concept where tokio::io::copy
will hang forever when switching between Cellular / WiFi / Wired networks if the reader is a reqwest::async_impl::Response
wrapped in a tokio_io::AsyncRead
using FuturesAsyncReadCompatExt
.
This happens on macOS and iOS, which are the platforms I have access to.
- Full proof-of-concept code (GitHub) (just
cargo run
) (UPDATE: This now contains a fix/workaround as well) - ProgressReadAdapter found here, but isn't necessary for the proof-of-concept.
- Previous attempt at getting help
- Further discussion on GitHub seanmonstar/reqwest
#[tokio::main()]
async fn main() {
let mut target_file = std::env::current_dir().unwrap();
target_file.push("bbb.mp4");
println!("File will be downloaded to {target_file:?}");
let client = ClientBuilder::default()
// Doesn't seem to help
.tcp_keepalive(Some(Duration::from_secs(1)))
// Doesn't seem to help
.connect_timeout(Duration::from_secs(1))
.build()
.unwrap();
let response = client.get("http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_native_60fps_stereo_abl.mp4").send().await.unwrap();
match response_to_file(response, target_file).await {
Ok(_) => println!("Everything OK"),
Err(err) => eprintln!("{err}"),
}
}
async fn response_to_file(response: Response, path: PathBuf) -> Result<(), ApiError> {
let download = response.bytes_stream();
let download = download
.map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e))
.into_async_read();
let download = download.compat();
// Wrap download to be able to get progress in terminal
let mut download = ProgressReadAdapter::new(download);
let temp_file = tokio::task::spawn_blocking(NamedTempFile::new)
.await
.wrap_api_err()?
.wrap_api_err()?;
let mut outfile = tokio::fs::File::create(temp_file.path())
.await
.wrap_api_err()?;
// Code hangs here forever after a network switch
tokio::io::copy(&mut download, &mut outfile)
.await
.wrap_api_err()?;
outfile.flush().await.wrap_api_err()?;
let _persisted_file: File = tokio::task::spawn_blocking(move || temp_file.persist(path))
.await
.wrap_api_err()?
.wrap_api_err()?;
Ok(())
}
There are some concepts in the code above, such as wrap_api_err
that can be found on GitHub, but I don't think they're important for analyzing the problem.
The main question is - How can I make response_to_file
exit with an Err
after switching networks?
The second question might be - If there is no easy way to fix this code, how do I make a streaming copy of a network resource to a temp file that actually exits cleanly when there is an error?