Skip to main content

cargo/util/
network.rs

1use anyhow::Error;
2
3use crate::util::errors::{CargoResult, HttpNot200};
4use crate::util::Config;
5
6pub struct Retry<'a> {
7    config: &'a Config,
8    remaining: u32,
9}
10
11impl<'a> Retry<'a> {
12    pub fn new(config: &'a Config) -> CargoResult<Retry<'a>> {
13        Ok(Retry {
14            config,
15            remaining: config.net_config()?.retry.unwrap_or(2),
16        })
17    }
18
19    pub fn r#try<T>(&mut self, f: impl FnOnce() -> CargoResult<T>) -> CargoResult<Option<T>> {
20        match f() {
21            Err(ref e) if maybe_spurious(e) && self.remaining > 0 => {
22                let msg = format!(
23                    "spurious network error ({} tries \
24                     remaining): {}",
25                    self.remaining, e
26                );
27                self.config.shell().warn(msg)?;
28                self.remaining -= 1;
29                Ok(None)
30            }
31            other => other.map(Some),
32        }
33    }
34}
35
36fn maybe_spurious(err: &Error) -> bool {
37    for e in err.chain() {
38        if let Some(git_err) = e.downcast_ref::<git2::Error>() {
39            match git_err.class() {
40                git2::ErrorClass::Net | git2::ErrorClass::Os => return true,
41                _ => (),
42            }
43        }
44        if let Some(curl_err) = e.downcast_ref::<curl::Error>() {
45            if curl_err.is_couldnt_connect()
46                || curl_err.is_couldnt_resolve_proxy()
47                || curl_err.is_couldnt_resolve_host()
48                || curl_err.is_operation_timedout()
49                || curl_err.is_recv_error()
50                || curl_err.is_http2_stream_error()
51                || curl_err.is_ssl_connect_error()
52                || curl_err.is_partial_file()
53            {
54                return true;
55            }
56        }
57        if let Some(not_200) = e.downcast_ref::<HttpNot200>() {
58            if 500 <= not_200.code && not_200.code < 600 {
59                return true;
60            }
61        }
62    }
63    false
64}
65
66/// Wrapper method for network call retry logic.
67///
68/// Retry counts provided by Config object `net.retry`. Config shell outputs
69/// a warning on per retry.
70///
71/// Closure must return a `CargoResult`.
72///
73/// # Examples
74///
75/// ```
76/// # use crate::cargo::util::{CargoResult, Config};
77/// # let download_something = || return Ok(());
78/// # let config = Config::default().unwrap();
79/// use cargo::util::network;
80/// let cargo_result = network::with_retry(&config, || download_something());
81/// ```
82pub fn with_retry<T, F>(config: &Config, mut callback: F) -> CargoResult<T>
83where
84    F: FnMut() -> CargoResult<T>,
85{
86    let mut retry = Retry::new(config)?;
87    loop {
88        if let Some(ret) = retry.r#try(&mut callback)? {
89            return Ok(ret);
90        }
91    }
92}
93
94#[test]
95fn with_retry_repeats_the_call_then_works() {
96    use crate::core::Shell;
97
98    //Error HTTP codes (5xx) are considered maybe_spurious and will prompt retry
99    let error1 = HttpNot200 {
100        code: 501,
101        url: "Uri".to_string(),
102    }
103    .into();
104    let error2 = HttpNot200 {
105        code: 502,
106        url: "Uri".to_string(),
107    }
108    .into();
109    let mut results: Vec<CargoResult<()>> = vec![Ok(()), Err(error1), Err(error2)];
110    let config = Config::default().unwrap();
111    *config.shell() = Shell::from_write(Box::new(Vec::new()));
112    let result = with_retry(&config, || results.pop().unwrap());
113    assert!(result.is_ok())
114}
115
116#[test]
117fn with_retry_finds_nested_spurious_errors() {
118    use crate::core::Shell;
119
120    //Error HTTP codes (5xx) are considered maybe_spurious and will prompt retry
121    //String error messages are not considered spurious
122    let error1 = anyhow::Error::from(HttpNot200 {
123        code: 501,
124        url: "Uri".to_string(),
125    });
126    let error1 = anyhow::Error::from(error1.context("A non-spurious wrapping err"));
127    let error2 = anyhow::Error::from(HttpNot200 {
128        code: 502,
129        url: "Uri".to_string(),
130    });
131    let error2 = anyhow::Error::from(error2.context("A second chained error"));
132    let mut results: Vec<CargoResult<()>> = vec![Ok(()), Err(error1), Err(error2)];
133    let config = Config::default().unwrap();
134    *config.shell() = Shell::from_write(Box::new(Vec::new()));
135    let result = with_retry(&config, || results.pop().unwrap());
136    assert!(result.is_ok())
137}
138
139#[test]
140fn curle_http2_stream_is_spurious() {
141    let code = curl_sys::CURLE_HTTP2_STREAM;
142    let err = curl::Error::new(code);
143    assert!(maybe_spurious(&err.into()));
144}