Skip to main content

cargo/util/network/
http_async.rs

1//! Async wrapper around cURL for making managing HTTP requests.
2//!
3//! Requests are executed in parallel using cURL [`Multi`] on
4//! a worker thread that is owned by the Client.
5
6use std::collections::HashMap;
7use std::io::Cursor;
8use std::io::Read;
9use std::str::FromStr;
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12use std::sync::mpsc;
13use std::sync::mpsc::Receiver;
14use std::sync::mpsc::Sender;
15use std::thread::JoinHandle;
16use std::time::Duration;
17use std::time::Instant;
18
19use curl::easy::Easy2;
20use curl::easy::Handler;
21use curl::easy::InfoType;
22use curl::easy::WriteError;
23use curl::multi::Easy2Handle;
24use curl::multi::Multi;
25use futures::channel::oneshot;
26use portable_atomic::AtomicI64;
27use portable_atomic::AtomicU64;
28use tracing::{debug, error, trace, warn};
29
30use crate::util::network::http::HandleConfiguration;
31use crate::util::network::http::HttpTimeout;
32
33type Response = http::Response<Vec<u8>>;
34type Request = http::Request<Vec<u8>>;
35type HttpResult<T> = std::result::Result<T, Error>;
36
37#[derive(Debug, Clone, thiserror::Error)]
38#[non_exhaustive]
39pub enum Error {
40    #[error("curl multi failed")]
41    Multi(#[from] curl::MultiError),
42
43    #[error("curl failed")]
44    Easy(#[from] curl::Error),
45
46    #[error(
47        "transfer too slow: failed to transfer more than {low_speed_limit} bytes in {}s (transferred {transferred} bytes)",
48        timeout_dur.as_secs()
49    )]
50    TooSlow {
51        low_speed_limit: u32,
52        timeout_dur: Duration,
53        transferred: u64,
54    },
55
56    #[error("failed to convert header value of `{name}` to string: {bytes:?}")]
57    BadHeader { name: String, bytes: Vec<u8> },
58}
59
60struct Message {
61    easy: Easy2<Collector>,
62    sender: oneshot::Sender<HttpResult<Response>>,
63}
64
65#[derive(Default)]
66struct Stats {
67    dl_remaining: AtomicI64,
68    dl_transferred: AtomicU64,
69}
70
71/// HTTP Client. Creating a new client spawns a cURL `Multi` and
72/// thread that is used for all HTTP requests by this client.
73pub struct Client {
74    channel: Option<Sender<Message>>,
75    thread_handle: Option<JoinHandle<()>>,
76    handle_config: HandleConfiguration,
77    stats: Arc<Stats>,
78}
79
80impl Client {
81    /// Spawns a new worker thread where HTTP request execute.
82    pub fn new(handle_config: HandleConfiguration) -> Client {
83        let (tx, rx) = mpsc::channel();
84        let stats = Arc::new(Stats::default());
85        let timeout = handle_config.timeout.clone();
86        let worker_stats = stats.clone();
87        let handle = std::thread::spawn(move || {
88            WorkerServer::run(rx, handle_config.multiplexing, timeout, worker_stats)
89        });
90        Client {
91            channel: Some(tx),
92            thread_handle: Some(handle),
93            handle_config,
94            stats,
95        }
96    }
97
98    /// Perform an HTTP request using this client.
99    pub async fn request(&self, request: Request) -> HttpResult<Response> {
100        let url = request.uri().to_string();
101        debug!(target: "network::fetch", url);
102        let mut collector = Collector::new(self.stats.clone());
103        let (parts, body) = request.into_parts();
104        let body_len = body.len();
105        collector.request_body = Cursor::new(body);
106        collector.debug = self.handle_config.verbose;
107        let mut handle = curl::easy::Easy2::new(collector);
108        self.handle_config.configure2(&mut handle)?;
109
110        handle.url(&url)?;
111        handle.follow_location(true)?;
112        handle.progress(true)?;
113
114        match parts.method {
115            http::Method::HEAD => handle.nobody(true)?,
116            http::Method::GET => handle.get(true)?,
117            http::Method::POST => {
118                handle.post_field_size(body_len as u64)?;
119                handle.post(true)?;
120            }
121            http::Method::PUT => {
122                handle.in_filesize(body_len as u64)?;
123                handle.put(true)?;
124            }
125            method => {
126                handle.upload(true)?;
127                handle.in_filesize(body_len as u64)?;
128                handle.custom_request(method.as_str())?;
129            }
130        }
131
132        let mut headers = curl::easy::List::new();
133        for (name, value) in parts.headers {
134            if let Some(name) = name {
135                let value: &str = value.to_str().map_err(|_| Error::BadHeader {
136                    name: name.to_string(),
137                    bytes: value.as_bytes().to_owned(),
138                })?;
139                headers.append(&format!("{}: {}", name, value))?;
140            }
141        }
142        handle.http_headers(headers)?;
143
144        let (sender, receiver) = oneshot::channel();
145        let req = Message {
146            easy: handle,
147            sender,
148        };
149
150        self.channel.as_ref().unwrap().send(req).unwrap();
151        receiver.await.unwrap()
152    }
153
154    /// Returns the number pending bytes across all active transfers.
155    pub fn bytes_pending(&self) -> u64 {
156        self.stats
157            .dl_remaining
158            .load(Ordering::Acquire)
159            .try_into()
160            .unwrap()
161    }
162}
163
164impl Drop for Client {
165    fn drop(&mut self) {
166        // Close the channel
167        drop(self.channel.take().unwrap());
168        // Join the thread
169        let _ = self.thread_handle.take().unwrap().join();
170    }
171}
172
173impl std::fmt::Debug for Client {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        f.debug_struct("http_async::Client").finish()
176    }
177}
178
179/// Manages the cURL `Multi`. Processes incoming work sent over the
180/// channel, and returns responses.
181struct WorkerServer {
182    /// Channel to receive new work
183    incoming_work: Receiver<Message>,
184    /// curl multi interface
185    multi: Multi,
186    /// Map of token to curl handle and response channel
187    handles: HashMap<
188        usize,
189        (
190            Easy2Handle<Collector>,
191            oneshot::Sender<HttpResult<Response>>,
192        ),
193    >,
194    /// Next token to use
195    token: usize,
196    /// Global timeout configuration
197    timeout: HttpTimeout,
198    /// Global transfer statistics
199    stats: Arc<Stats>,
200    /// Instant when the current low speed window started
201    low_speed_window_start: Instant,
202    /// Amount of total bytes transferred when the current low speed window started
203    low_speed_window_initial: u64,
204}
205
206impl WorkerServer {
207    fn run(
208        incoming_work: Receiver<Message>,
209        multiplex: bool,
210        timeout: HttpTimeout,
211        stats: Arc<Stats>,
212    ) {
213        let mut multi = Multi::new();
214        // let's not flood the server with connections
215        if let Err(e) = multi.set_max_host_connections(2) {
216            error!("failed to set max host connections in curl: {e}");
217        }
218        if let Err(e) = multi.pipelining(false, multiplex) {
219            error!("failed to enable multiplexing/pipelining in curl: {e}");
220        }
221
222        let mut worker = Self {
223            incoming_work,
224            multi,
225            handles: HashMap::new(),
226            token: 0,
227            timeout,
228            stats,
229            low_speed_window_start: Instant::now(),
230            low_speed_window_initial: 0,
231        };
232        worker.worker_loop();
233    }
234
235    fn fail_and_drain(&mut self, e: &Error) {
236        warn!(
237            target: "network",
238            "failing all outstanding HTTP requests: {e}"
239        );
240        for (_token, (_handle, sender)) in self.handles.drain() {
241            let _ = sender.send(Err(e.clone()));
242        }
243    }
244
245    /// Marks the start of a new timeout window.
246    fn reset_low_speed_timeout(&mut self) {
247        self.low_speed_window_start = Instant::now();
248        self.low_speed_window_initial = self.stats.dl_transferred.load(Ordering::Acquire);
249    }
250
251    /// Return an error if we're at the end of a timeout window, we haven't
252    /// made enough progress.
253    fn check_low_speed_timeout(&mut self) -> Option<Error> {
254        // Make sure we've waited for the timeout duration
255        if Instant::now().duration_since(self.low_speed_window_start) < self.timeout.dur {
256            return None;
257        }
258
259        // Calculate how much we've transferred since the last check.
260        let current = self.stats.dl_transferred.load(Ordering::Acquire);
261        let transferred = current.saturating_sub(self.low_speed_window_initial);
262        self.reset_low_speed_timeout();
263        if transferred < self.timeout.low_speed_limit.into() {
264            Some(Error::TooSlow {
265                low_speed_limit: self.timeout.low_speed_limit,
266                timeout_dur: self.timeout.dur,
267                transferred,
268            })
269        } else {
270            None
271        }
272    }
273
274    fn worker_loop(&mut self) {
275        const INITIAL_DELAY: Duration = Duration::from_millis(1);
276        let mut wait_backoff = INITIAL_DELAY;
277        loop {
278            // Start any pending work.
279            while let Ok(msg) = self.incoming_work.try_recv() {
280                self.enqueue_request(msg);
281                wait_backoff = INITIAL_DELAY;
282            }
283
284            match self.multi.perform() {
285                Err(e) if e.is_call_perform() => {
286                    // cURL states if you receive `is_call_perform`, this means that you should call `perform` again.
287                }
288                Err(e) => {
289                    self.fail_and_drain(&Error::Multi(e));
290                }
291                Ok(running) => {
292                    self.multi.messages(|msg| {
293                        let t = msg.token().expect("all handles have tokens");
294                        trace!(token = t, "finish");
295                        let Some((handle, sender)) = self.handles.remove(&t) else {
296                            error!("missing entry {t} in handle table");
297                            return;
298                        };
299                        let result = msg.result_for2(&handle).expect("handle must have a result");
300                        let mut easy = self.multi.remove2(handle).expect("handle must be in multi");
301                        let mut response = std::mem::replace(
302                            &mut easy.get_mut().response,
303                            Response::new(Vec::new()),
304                        );
305                        if let Ok(status) = easy.response_code()
306                            && status != 0
307                            && let Ok(status) = http::StatusCode::from_u16(status as u16)
308                        {
309                            *response.status_mut() = status;
310                        }
311                        // Would be nice to set HTTP version via `response.version_mut()`, but `curl` doesn't have it exposed.
312                        let extensions = Extensions {
313                            client_ip: easy.primary_ip().ok().flatten().map(str::to_string),
314                        };
315                        response.extensions_mut().insert(extensions);
316                        let _ = sender.send(result.map(|()| response).map_err(Into::into));
317                    });
318
319                    if running > 0 {
320                        // Check for low speed timeout.
321                        if let Some(timeout_error) = self.check_low_speed_timeout() {
322                            self.fail_and_drain(&timeout_error);
323                            continue;
324                        }
325
326                        let max_timeout = Duration::from_millis(1000);
327                        let mut timeout = self
328                            .multi
329                            .get_timeout()
330                            .ok()
331                            .flatten()
332                            .unwrap_or(max_timeout)
333                            .min(max_timeout);
334                        if timeout.is_zero() {
335                            // curl said not to wait.
336                            continue;
337                        }
338                        // Ideally we would use `Multi::poll` + a `MultiWaker` instead of `Multi::wait`
339                        // to wake the thread when new work is queued. But it requires curl 7.68+,
340                        // which is not available everywhere we support.
341                        //
342                        // Instead, we use an exponential backoff approach so that as long as requests
343                        // are being queued, we poll quickly to allow the requests to be added sooner.
344                        // Without this, we end up sitting in `Multi::wait` too long while new work is
345                        // added to the channel.
346                        //
347                        // `get_timeout` says we should wait *at most* the timeout amount, so reducing
348                        // the wait time is fine.
349                        if wait_backoff < timeout {
350                            wait_backoff *= 2;
351                            timeout = wait_backoff
352                        }
353                        trace!(
354                            pending = self.handles.len(),
355                            timeout = timeout.as_millis(),
356                            "curl wait"
357                        );
358                        if let Err(e) = self.multi.wait(&mut [], timeout) {
359                            self.fail_and_drain(&Error::Multi(e));
360                        }
361                    } else {
362                        // Block, waiting for more work
363                        trace!("all work completed");
364                        match self.incoming_work.recv() {
365                            Ok(msg) => {
366                                trace!("resuming work");
367                                self.reset_low_speed_timeout();
368                                self.enqueue_request(msg);
369                                wait_backoff = INITIAL_DELAY;
370                            }
371                            Err(_) => {
372                                // The sending channel is closed. Shut down the worker.
373                                break;
374                            }
375                        }
376                    }
377                }
378            }
379        }
380    }
381
382    /// Adds the request to the `Multi`, or send an error back through the channel.
383    fn enqueue_request(&mut self, message: Message) {
384        match self.multi.add2(message.easy) {
385            Ok(mut handle) => {
386                self.token = self.token.wrapping_add(1);
387                handle.set_token(self.token).ok();
388                self.handles.insert(self.token, (handle, message.sender));
389            }
390            Err(e) => {
391                let _ = message.sender.send(Err(e.into()));
392            }
393        }
394    }
395}
396
397/// Interface that cURL (`Easy2`) uses to make progress.
398struct Collector {
399    /// The response being built
400    response: Response,
401    /// The body to transmit
402    request_body: Cursor<Vec<u8>>,
403    /// Whether we're in debug mode
404    debug: bool,
405    /// Global transfer statistics.
406    global_stats: Arc<Stats>,
407    /// How much has this particular transfer added to global `dl_remaining` stats.
408    dl_remaining_delta: i64,
409}
410
411impl Collector {
412    fn new(stats: Arc<Stats>) -> Self {
413        Collector {
414            response: Response::new(Vec::new()),
415            request_body: Cursor::new(Vec::new()),
416            debug: false,
417            global_stats: stats,
418            dl_remaining_delta: 0,
419        }
420    }
421}
422
423impl Handler for Collector {
424    fn write(&mut self, data: &[u8]) -> Result<usize, WriteError> {
425        self.response.body_mut().extend_from_slice(data);
426        self.global_stats
427            .dl_transferred
428            .fetch_add(data.len() as u64, Ordering::Release);
429        Ok(data.len())
430    }
431
432    fn header(&mut self, data: &[u8]) -> bool {
433        if let Some((name, value)) = handle_http_header(data)
434            && let Ok(name) = http::HeaderName::from_str(name)
435            && let Ok(value) = http::HeaderValue::from_str(value)
436        {
437            self.response.headers_mut().append(name, value);
438        }
439        true
440    }
441
442    fn read(&mut self, data: &mut [u8]) -> Result<usize, curl::easy::ReadError> {
443        Ok(self.request_body.read(data).unwrap())
444    }
445
446    fn debug(&mut self, kind: InfoType, data: &[u8]) {
447        if self.debug {
448            super::http::debug(kind, data);
449        }
450    }
451
452    fn progress(&mut self, dltotal: f64, dlnow: f64, _ultotal: f64, _ulnow: f64) -> bool {
453        if dlnow > dltotal {
454            return true;
455        }
456        let dl_total = dltotal as i64;
457        let dl_current = dlnow as i64;
458
459        let remaining = dl_total - dl_current;
460
461        self.global_stats
462            .dl_remaining
463            .fetch_add(remaining - self.dl_remaining_delta, Ordering::Release);
464        self.dl_remaining_delta = remaining;
465        true
466    }
467}
468
469impl Drop for Collector {
470    fn drop(&mut self) {
471        // Zero out this transfer's contribution to the global dl_remaining.
472        self.global_stats
473            .dl_remaining
474            .fetch_add(-self.dl_remaining_delta, Ordering::Release);
475    }
476}
477
478/// Additional fields on an [`http::Response`].
479#[derive(Clone)]
480struct Extensions {
481    client_ip: Option<String>,
482}
483
484pub trait ResponsePartsExtensions {
485    fn client_ip(&self) -> Option<&str>;
486}
487
488impl ResponsePartsExtensions for http::response::Parts {
489    fn client_ip(&self) -> Option<&str> {
490        self.extensions
491            .get::<Extensions>()
492            .and_then(|extensions| extensions.client_ip.as_deref())
493    }
494}
495
496impl ResponsePartsExtensions for Response {
497    fn client_ip(&self) -> Option<&str> {
498        self.extensions()
499            .get::<Extensions>()
500            .and_then(|extensions| extensions.client_ip.as_deref())
501    }
502}
503
504/// Splits HTTP `HEADER: VALUE` to a tuple.
505fn handle_http_header(buf: &[u8]) -> Option<(&str, &str)> {
506    if buf.is_empty() {
507        return None;
508    }
509    let buf = std::str::from_utf8(buf).ok()?.trim_end();
510    // Don't let server sneak extra lines anywhere.
511    if buf.contains('\n') {
512        return None;
513    }
514    let (tag, value) = buf.split_once(':')?;
515    let value = value.trim();
516    Some((tag, value))
517}