Time out every HTTP connection instead of waiting forever
No ureq agent set a timeout, and ureq sets none by default, so a connection dropped silently mid-transfer (a NAT or load-balancer idle drop) hung a JMAP, DAV, EWS or Graph run, or an export, with no error, and the retry logic never got a chance to run. IMAP and ManageSieve already had read timeouts. Every agent now takes its settings from a new net module: 30s to connect, 60s to send the request headers, 5 minutes for the server's first byte, and 30 minutes for a whole response body, which ureq counts as one budget for the body rather than per read: enough for the 512 MiB limit at about 300 KB/s. A JMAP upload's send budget grows with its size, from a 2 minute floor at an assumed 64 KiB/s worst case. A timeout is a transport error, and every client already retries those, so a stalled transfer is now abandoned and retried. Tests pin that down for each client. The TLS setup the seven agents repeated moves to one helper.
This commit is contained in:
@@ -13,7 +13,6 @@ use std::time::{Duration, Instant};
|
||||
use serde_json::Value;
|
||||
use ureq::Agent;
|
||||
use ureq::config::{Config, RedirectAuthHeaders};
|
||||
use ureq::tls::{RootCerts, TlsConfig};
|
||||
use ureq::{ResponseExt, http::Uri};
|
||||
|
||||
use crate::exchange_graph::error::GraphError;
|
||||
@@ -21,6 +20,7 @@ use crate::exchange_graph::retry::{HttpClass, classify_http_status, is_throttled
|
||||
use crate::jmap::http::{RetryPolicy, cross_host, retry_after_header};
|
||||
use crate::jmap::retry::{self, RateLimitState};
|
||||
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
|
||||
use crate::net::{tls, with_timeouts};
|
||||
|
||||
const MAX_BODY: u64 = 256 * 1024 * 1024;
|
||||
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
|
||||
@@ -90,19 +90,13 @@ enum Attempt {
|
||||
|
||||
impl GraphClient {
|
||||
pub fn new(bearer: String, retry: RetryPolicy, allow_invalid_certs: bool) -> GraphClient {
|
||||
let config: Config = Config::builder()
|
||||
.http_status_as_error(false)
|
||||
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
||||
.tls_config(
|
||||
TlsConfig::builder()
|
||||
.unversioned_rustls_crypto_provider(std::sync::Arc::new(
|
||||
rustls::crypto::aws_lc_rs::default_provider(),
|
||||
))
|
||||
.root_certs(RootCerts::PlatformVerifier)
|
||||
.disable_verification(allow_invalid_certs)
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
let config: Config = with_timeouts!(
|
||||
Config::builder()
|
||||
.http_status_as_error(false)
|
||||
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
|
||||
.tls_config(tls(allow_invalid_certs))
|
||||
)
|
||||
.build();
|
||||
GraphClient {
|
||||
inner: Arc::new(Inner {
|
||||
agent: config.new_agent(),
|
||||
@@ -475,6 +469,21 @@ fn format_retry_wait(d: Duration) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_timeout_is_a_transport_error_and_so_retried() {
|
||||
// `execute` retries every GraphError::Transport; only Connect is fatal.
|
||||
for t in [
|
||||
ureq::Timeout::Connect,
|
||||
ureq::Timeout::SendRequest,
|
||||
ureq::Timeout::SendBody,
|
||||
ureq::Timeout::RecvResponse,
|
||||
ureq::Timeout::RecvBody,
|
||||
] {
|
||||
let err = map_ureq_error(ureq::Error::Timeout(t));
|
||||
assert!(matches!(err, GraphError::Transport(_)), "{t:?} -> {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_construct() {
|
||||
let c = GraphClient::new("token".to_owned(), RetryPolicy::new(3), false);
|
||||
|
||||
Reference in New Issue
Block a user