isahc/client.rs
1//! The HTTP client implementation.
2
3use crate::{
4 agent::{self, AgentBuilder},
5 body::{AsyncBody, Body},
6 config::{
7 client::ClientConfig,
8 request::{RequestConfig, SetOpt, WithRequestConfig},
9 *,
10 },
11 default_headers::DefaultHeadersInterceptor,
12 error::{Error, ErrorKind},
13 handler::{RequestHandler, ResponseBodyReader},
14 headers::HasHeaders,
15 interceptor::{self, Interceptor, InterceptorObj},
16 parsing::header_to_curl_string,
17};
18use futures_lite::{
19 future::{block_on, try_zip},
20 io::AsyncRead,
21};
22use http::{
23 header::{HeaderMap, HeaderName, HeaderValue},
24 Request,
25 Response,
26};
27use once_cell::sync::Lazy;
28use std::{
29 convert::TryFrom,
30 fmt,
31 future::Future,
32 io,
33 pin::Pin,
34 sync::Arc,
35 task::{Context, Poll},
36 time::Duration,
37};
38use tracing_futures::Instrument;
39
40static USER_AGENT: Lazy<String> = Lazy::new(|| {
41 format!(
42 "curl/{} isahc/{}",
43 curl::Version::get().version(),
44 env!("CARGO_PKG_VERSION")
45 )
46});
47
48/// An HTTP client builder, capable of creating custom [`HttpClient`] instances
49/// with customized behavior.
50///
51/// Any option that can be configured per-request can also be configured on a
52/// client builder as a default setting. Request configuration is provided by
53/// the [`Configurable`] trait, which is also available in the
54/// [`prelude`](crate::prelude) module.
55///
56/// # Examples
57///
58/// ```
59/// use isahc::{
60/// config::{RedirectPolicy, VersionNegotiation},
61/// prelude::*,
62/// HttpClient,
63/// };
64/// use std::time::Duration;
65///
66/// let client = HttpClient::builder()
67/// .timeout(Duration::from_secs(60))
68/// .redirect_policy(RedirectPolicy::Limit(10))
69/// .version_negotiation(VersionNegotiation::http2())
70/// .build()?;
71/// # Ok::<(), isahc::Error>(())
72/// ```
73#[must_use = "builders have no effect if unused"]
74pub struct HttpClientBuilder {
75 agent_builder: AgentBuilder,
76 client_config: ClientConfig,
77 request_config: RequestConfig,
78 interceptors: Vec<InterceptorObj>,
79 default_headers: HeaderMap<HeaderValue>,
80 error: Option<Error>,
81
82 #[cfg(feature = "cookies")]
83 cookie_jar: Option<crate::cookies::CookieJar>,
84}
85
86impl Default for HttpClientBuilder {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl HttpClientBuilder {
93 /// Create a new builder for building a custom client. All configuration
94 /// will start out with the default values.
95 ///
96 /// This is equivalent to the [`Default`] implementation.
97 pub fn new() -> Self {
98 Self {
99 agent_builder: AgentBuilder::default(),
100 client_config: ClientConfig::default(),
101 request_config: RequestConfig::client_defaults(),
102 interceptors: vec![
103 // Add redirect support. Note that this is _always_ the first,
104 // and thus the outermost, interceptor. Also note that this does
105 // not enable redirect following, it just implements support for
106 // it, if a request asks for it.
107 InterceptorObj::new(crate::redirect::RedirectInterceptor),
108 ],
109 default_headers: HeaderMap::new(),
110 error: None,
111
112 #[cfg(feature = "cookies")]
113 cookie_jar: None,
114 }
115 }
116
117 /// Enable persistent cookie handling for all requests using this client
118 /// using a shared cookie jar.
119 ///
120 /// # Examples
121 ///
122 /// ```no_run
123 /// use isahc::{prelude::*, HttpClient};
124 ///
125 /// // Create a client with a cookie jar.
126 /// let client = HttpClient::builder()
127 /// .cookies()
128 /// .build()?;
129 ///
130 /// // Make a request that sets a cookie.
131 /// let uri = "http://httpbin.org/cookies/set?foo=bar".parse()?;
132 /// client.get(&uri)?;
133 ///
134 /// // Get the cookie from the cookie jar.
135 /// let cookie = client.cookie_jar()
136 /// .unwrap()
137 /// .get_by_name(&uri, "foo")
138 /// .unwrap();
139 /// assert_eq!(cookie, "bar");
140 ///
141 /// # Ok::<(), Box<dyn std::error::Error>>(())
142 /// ```
143 ///
144 /// # Availability
145 ///
146 /// This method is only available when the [`cookies`](index.html#cookies)
147 /// feature is enabled.
148 #[cfg(feature = "cookies")]
149 pub fn cookies(self) -> Self {
150 // Note: this method is now essentially the same as setting a default
151 // cookie jar, but remains for backwards compatibility.
152 self.cookie_jar(Default::default())
153 }
154
155 /// Add a request interceptor to the client.
156 ///
157 /// # Availability
158 ///
159 /// This method is only available when the
160 /// [`unstable-interceptors`](index.html#unstable-interceptors) feature is
161 /// enabled.
162 #[cfg(feature = "unstable-interceptors")]
163 #[inline]
164 pub fn interceptor(self, interceptor: impl Interceptor + 'static) -> Self {
165 self.interceptor_impl(interceptor)
166 }
167
168 #[allow(unused)]
169 pub(crate) fn interceptor_impl(mut self, interceptor: impl Interceptor + 'static) -> Self {
170 self.interceptors.push(InterceptorObj::new(interceptor));
171 self
172 }
173
174 /// Set a maximum number of simultaneous connections that this client is
175 /// allowed to keep open at one time.
176 ///
177 /// If set to a value greater than zero, no more than `max` connections will
178 /// be opened at one time. If executing a new request would require opening
179 /// a new connection, then the request will stay in a "pending" state until
180 /// an existing connection can be used or an active request completes and
181 /// can be closed, making room for a new connection.
182 ///
183 /// Setting this value to `0` disables the limit entirely.
184 ///
185 /// This is an effective way of limiting the number of sockets or file
186 /// descriptors that this client will open, though note that the client may
187 /// use file descriptors for purposes other than just HTTP connections.
188 ///
189 /// By default this value is `0` and no limit is enforced.
190 ///
191 /// To apply a limit per-host, see
192 /// [`HttpClientBuilder::max_connections_per_host`].
193 pub fn max_connections(mut self, max: usize) -> Self {
194 self.agent_builder = self.agent_builder.max_connections(max);
195 self
196 }
197
198 /// Set a maximum number of simultaneous connections that this client is
199 /// allowed to keep open to individual hosts at one time.
200 ///
201 /// If set to a value greater than zero, no more than `max` connections will
202 /// be opened to a single host at one time. If executing a new request would
203 /// require opening a new connection, then the request will stay in a
204 /// "pending" state until an existing connection can be used or an active
205 /// request completes and can be closed, making room for a new connection.
206 ///
207 /// Setting this value to `0` disables the limit entirely. By default this
208 /// value is `0` and no limit is enforced.
209 ///
210 /// To set a global limit across all hosts, see
211 /// [`HttpClientBuilder::max_connections`].
212 pub fn max_connections_per_host(mut self, max: usize) -> Self {
213 self.agent_builder = self.agent_builder.max_connections_per_host(max);
214 self
215 }
216
217 /// Set the size of the connection cache.
218 ///
219 /// After requests are completed, if the underlying connection is reusable,
220 /// it is added to the connection cache to be reused to reduce latency for
221 /// future requests.
222 ///
223 /// Setting the size to `0` disables connection caching for all requests
224 /// using this client.
225 ///
226 /// By default this value is unspecified. A reasonable default size will be
227 /// chosen.
228 pub fn connection_cache_size(mut self, size: usize) -> Self {
229 self.agent_builder = self.agent_builder.connection_cache_size(size);
230 self.client_config.close_connections = size == 0;
231 self
232 }
233
234 /// Set the maximum time-to-live (TTL) for connections to remain in the
235 /// connection cache.
236 ///
237 /// After requests are completed, if the underlying connection is
238 /// reusable, it is added to the connection cache to be reused to reduce
239 /// latency for future requests. This option controls how long such
240 /// connections should be still considered valid before being discarded.
241 ///
242 /// Old connections have a high risk of not working any more and thus
243 /// attempting to use them wastes time if the server has disconnected.
244 ///
245 /// The default TTL is 118 seconds.
246 pub fn connection_cache_ttl(mut self, ttl: Duration) -> Self {
247 self.client_config.connection_cache_ttl = Some(ttl);
248 self
249 }
250
251 /// Configure DNS caching.
252 ///
253 /// By default, DNS entries are cached by the client executing the request
254 /// and are used until the entry expires. Calling this method allows you to
255 /// change the entry timeout duration or disable caching completely.
256 ///
257 /// Note that DNS entry TTLs are not respected, regardless of this setting.
258 ///
259 /// By default caching is enabled with a 60 second timeout.
260 ///
261 /// # Examples
262 ///
263 /// ```
264 /// use isahc::{config::*, prelude::*, HttpClient};
265 /// use std::time::Duration;
266 ///
267 /// let client = HttpClient::builder()
268 /// // Cache entries for 10 seconds.
269 /// .dns_cache(Duration::from_secs(10))
270 /// // Cache entries forever.
271 /// .dns_cache(DnsCache::Forever)
272 /// // Don't cache anything.
273 /// .dns_cache(DnsCache::Disable)
274 /// .build()?;
275 /// # Ok::<(), isahc::Error>(())
276 /// ```
277 pub fn dns_cache<C>(mut self, cache: C) -> Self
278 where
279 C: Into<DnsCache>,
280 {
281 // This option is technically supported per-request by curl, but we only
282 // expose it on the client. Since the DNS cache is shared between all
283 // requests, exposing this option per-request would actually cause the
284 // timeout to alternate values for every request with a different
285 // timeout, resulting in some confusing (but valid) behavior.
286 self.client_config.dns_cache = Some(cache.into());
287 self
288 }
289
290 /// Set a mapping of DNS resolve overrides.
291 ///
292 /// Entries in the given map will be used first before using the default DNS
293 /// resolver for host+port pairs.
294 ///
295 /// Note that DNS resolving is only performed when establishing a new
296 ///
297 /// # Examples
298 ///
299 /// ```
300 /// use isahc::{config::ResolveMap, prelude::*, HttpClient};
301 /// use std::net::IpAddr;
302 ///
303 /// let client = HttpClient::builder()
304 /// .dns_resolve(ResolveMap::new()
305 /// // Send requests for example.org on port 80 to 127.0.0.1.
306 /// .add("example.org", 80, [127, 0, 0, 1]))
307 /// .build()?;
308 /// # Ok::<(), Box<dyn std::error::Error>>(())
309 /// ```
310 pub fn dns_resolve(mut self, map: ResolveMap) -> Self {
311 // Similar to the dns_cache option, this operation actually affects all
312 // requests in a multi handle so we do not expose it per-request to
313 // avoid confusing behavior.
314 self.client_config.dns_resolve = Some(map);
315 self
316 }
317
318 /// Add a default header to be passed with every request.
319 ///
320 /// If a default header value is already defined for the given key, then a
321 /// second header value will be appended to the list and multiple header
322 /// values will be included in the request.
323 ///
324 /// If any values are defined for this header key on an outgoing request,
325 /// they will override any default header values.
326 ///
327 /// If the header key or value are malformed, [`HttpClientBuilder::build`]
328 /// will return an error.
329 ///
330 /// # Examples
331 ///
332 /// ```
333 /// use isahc::{prelude::*, HttpClient};
334 ///
335 /// let client = HttpClient::builder()
336 /// .default_header("some-header", "some-value")
337 /// .build()?;
338 /// # Ok::<(), Box<dyn std::error::Error>>(())
339 /// ```
340 pub fn default_header<K, V>(mut self, key: K, value: V) -> Self
341 where
342 HeaderName: TryFrom<K>,
343 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
344 HeaderValue: TryFrom<V>,
345 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
346 {
347 match HeaderName::try_from(key) {
348 Ok(key) => match HeaderValue::try_from(value) {
349 Ok(value) => {
350 self.default_headers.append(key, value);
351 }
352 Err(e) => {
353 self.error = Some(Error::new(ErrorKind::ClientInitialization, e.into()));
354 }
355 },
356 Err(e) => {
357 self.error = Some(Error::new(ErrorKind::ClientInitialization, e.into()));
358 }
359 }
360 self
361 }
362
363 /// Set the default headers to include in every request, replacing any
364 /// previously set default headers.
365 ///
366 /// Headers defined on an individual request always override headers in the
367 /// default map.
368 ///
369 /// If any header keys or values are malformed, [`HttpClientBuilder::build`]
370 /// will return an error.
371 ///
372 /// # Examples
373 ///
374 /// Set default headers from a slice:
375 ///
376 /// ```
377 /// use isahc::{prelude::*, HttpClient};
378 ///
379 /// let mut builder = HttpClient::builder()
380 /// .default_headers(&[
381 /// ("some-header", "value1"),
382 /// ("some-header", "value2"),
383 /// ("some-other-header", "some-other-value"),
384 /// ])
385 /// .build()?;
386 /// # Ok::<(), Box<dyn std::error::Error>>(())
387 /// ```
388 ///
389 /// Using an existing header map:
390 ///
391 /// ```
392 /// use isahc::{prelude::*, HttpClient};
393 ///
394 /// let mut headers = http::HeaderMap::new();
395 /// headers.append("some-header".parse::<http::header::HeaderName>()?, "some-value".parse()?);
396 ///
397 /// let mut builder = HttpClient::builder()
398 /// .default_headers(&headers)
399 /// .build()?;
400 /// # Ok::<(), Box<dyn std::error::Error>>(())
401 /// ```
402 ///
403 /// Using a hashmap:
404 ///
405 /// ```
406 /// use isahc::{prelude::*, HttpClient};
407 /// use std::collections::HashMap;
408 ///
409 /// let mut headers = HashMap::new();
410 /// headers.insert("some-header", "some-value");
411 ///
412 /// let mut builder = HttpClient::builder()
413 /// .default_headers(headers)
414 /// .build()?;
415 /// # Ok::<(), Box<dyn std::error::Error>>(())
416 /// ```
417 pub fn default_headers<K, V, I, P>(mut self, headers: I) -> Self
418 where
419 HeaderName: TryFrom<K>,
420 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
421 HeaderValue: TryFrom<V>,
422 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
423 I: IntoIterator<Item = P>,
424 P: HeaderPair<K, V>,
425 {
426 self.default_headers.clear();
427
428 for (key, value) in headers.into_iter().map(HeaderPair::pair) {
429 self = self.default_header(key, value);
430 }
431
432 self
433 }
434
435 /// Build an [`HttpClient`] using the configured options.
436 ///
437 /// If the client fails to initialize, an error will be returned.
438 #[allow(unused_mut)]
439 pub fn build(mut self) -> Result<HttpClient, Error> {
440 if let Some(err) = self.error {
441 return Err(err);
442 }
443
444 // Add cookie interceptor if enabled.
445 #[cfg(feature = "cookies")]
446 {
447 let jar = self.cookie_jar.clone();
448 self = self.interceptor_impl(crate::cookies::interceptor::CookieInterceptor::new(jar));
449 }
450
451 // Add default header interceptor if any default headers were specified.
452 if !self.default_headers.is_empty() {
453 let default_headers = std::mem::take(&mut self.default_headers);
454 self = self.interceptor_impl(DefaultHeadersInterceptor::from(default_headers));
455 }
456
457 #[cfg(not(feature = "cookies"))]
458 let inner = Inner {
459 agent: self
460 .agent_builder
461 .spawn()
462 .map_err(|e| Error::new(ErrorKind::ClientInitialization, e))?,
463 client_config: self.client_config,
464 request_config: self.request_config,
465 interceptors: self.interceptors,
466 };
467
468 #[cfg(feature = "cookies")]
469 let inner = Inner {
470 agent: self
471 .agent_builder
472 .spawn()
473 .map_err(|e| Error::new(ErrorKind::ClientInitialization, e))?,
474 client_config: self.client_config,
475 request_config: self.request_config,
476 interceptors: self.interceptors,
477 cookie_jar: self.cookie_jar,
478 };
479
480 Ok(HttpClient {
481 inner: Arc::new(inner),
482 })
483 }
484}
485
486impl Configurable for HttpClientBuilder {
487 #[cfg(feature = "cookies")]
488 fn cookie_jar(mut self, cookie_jar: crate::cookies::CookieJar) -> Self {
489 self.cookie_jar = Some(cookie_jar);
490 self
491 }
492}
493
494impl WithRequestConfig for HttpClientBuilder {
495 #[inline]
496 fn with_config(mut self, f: impl FnOnce(&mut RequestConfig)) -> Self {
497 f(&mut self.request_config);
498 self
499 }
500}
501
502impl fmt::Debug for HttpClientBuilder {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.debug_struct("HttpClientBuilder").finish()
505 }
506}
507
508/// Helper trait for defining key-value pair types that can be dereferenced into
509/// a tuple from a reference.
510///
511/// This trait is sealed and cannot be implemented for types outside of Isahc.
512pub trait HeaderPair<K, V> {
513 fn pair(self) -> (K, V);
514}
515
516impl<K, V> HeaderPair<K, V> for (K, V) {
517 fn pair(self) -> (K, V) {
518 self
519 }
520}
521
522impl<'a, K: Copy, V: Copy> HeaderPair<K, V> for &'a (K, V) {
523 fn pair(self) -> (K, V) {
524 (self.0, self.1)
525 }
526}
527
528/// An HTTP client for making requests.
529///
530/// An [`HttpClient`] instance acts as a session for executing one or more HTTP
531/// requests, and also allows you to set common protocol settings that should be
532/// applied to all requests made with the client.
533///
534/// [`HttpClient`] is entirely thread-safe, and implements both [`Send`] and
535/// [`Sync`]. You are free to create clients outside the context of the "main"
536/// thread, or move them between threads. You can even invoke many requests
537/// simultaneously from multiple threads, since doing so doesn't need a mutable
538/// reference to the client. This is fairly cheap to do as well, since
539/// internally requests use lock-free message passing to get things going.
540///
541/// The client maintains a connection pool internally and is not cheap to
542/// create, so we recommend creating a client once and re-using it throughout
543/// your code. Creating a new client for every request would decrease
544/// performance significantly, and might cause errors to occur under high
545/// workloads, caused by creating too many system resources like sockets or
546/// threads.
547///
548/// It is not universally true that you should use exactly one client instance
549/// in an application. All HTTP requests made with the same client will share
550/// any session-wide state, like cookies or persistent connections. It may be
551/// the case that it is better to create separate clients for separate areas of
552/// an application if they have separate concerns or are making calls to
553/// different servers. If you are creating an API client library, that might be
554/// a good place to maintain your own internal client.
555///
556/// # Examples
557///
558/// ```no_run
559/// use isahc::{prelude::*, HttpClient};
560///
561/// // Create a new client using reasonable defaults.
562/// let client = HttpClient::new()?;
563///
564/// // Make some requests.
565/// let mut response = client.get("https://example.org")?;
566/// assert!(response.status().is_success());
567///
568/// println!("Response:\n{}", response.text()?);
569/// # Ok::<(), isahc::Error>(())
570/// ```
571///
572/// Customizing the client configuration:
573///
574/// ```no_run
575/// use isahc::{
576/// config::{RedirectPolicy, VersionNegotiation},
577/// prelude::*,
578/// HttpClient,
579/// };
580/// use std::time::Duration;
581///
582/// let client = HttpClient::builder()
583/// .version_negotiation(VersionNegotiation::http11())
584/// .redirect_policy(RedirectPolicy::Limit(10))
585/// .timeout(Duration::from_secs(20))
586/// // May return an error if there's something wrong with our configuration
587/// // or if the client failed to start up.
588/// .build()?;
589///
590/// let response = client.get("https://example.org")?;
591/// assert!(response.status().is_success());
592/// # Ok::<(), isahc::Error>(())
593/// ```
594///
595/// See the documentation on [`HttpClientBuilder`] for a comprehensive look at
596/// what can be configured.
597#[derive(Clone)]
598pub struct HttpClient {
599 inner: Arc<Inner>,
600}
601
602struct Inner {
603 /// This is how we talk to our background agent thread.
604 agent: agent::Handle,
605
606 /// Client-wide request configuration.
607 client_config: ClientConfig,
608
609 /// Default request configuration to use if not specified in a request.
610 request_config: RequestConfig,
611
612 /// Registered interceptors that requests should pass through.
613 interceptors: Vec<InterceptorObj>,
614
615 /// Configured cookie jar, if any.
616 #[cfg(feature = "cookies")]
617 cookie_jar: Option<crate::cookies::CookieJar>,
618}
619
620impl HttpClient {
621 /// Create a new HTTP client using the default configuration.
622 ///
623 /// If the client fails to initialize, an error will be returned.
624 pub fn new() -> Result<Self, Error> {
625 HttpClientBuilder::default().build()
626 }
627
628 /// Get a reference to a global client instance.
629 ///
630 /// TODO: Stabilize.
631 pub(crate) fn shared() -> &'static Self {
632 static SHARED: Lazy<HttpClient> =
633 Lazy::new(|| HttpClient::new().expect("shared client failed to initialize"));
634
635 &SHARED
636 }
637
638 /// Create a new [`HttpClientBuilder`] for building a custom client.
639 pub fn builder() -> HttpClientBuilder {
640 HttpClientBuilder::default()
641 }
642
643 /// Get the configured cookie jar for this HTTP client, if any.
644 ///
645 /// # Availability
646 ///
647 /// This method is only available when the [`cookies`](index.html#cookies)
648 /// feature is enabled.
649 #[cfg(feature = "cookies")]
650 pub fn cookie_jar(&self) -> Option<&crate::cookies::CookieJar> {
651 self.inner.cookie_jar.as_ref()
652 }
653
654 /// Send a GET request to the given URI.
655 ///
656 /// To customize the request further, see [`HttpClient::send`]. To execute
657 /// the request asynchronously, see [`HttpClient::get_async`].
658 ///
659 /// # Examples
660 ///
661 /// ```no_run
662 /// use isahc::{prelude::*, HttpClient};
663 ///
664 /// let client = HttpClient::new()?;
665 /// let mut response = client.get("https://example.org")?;
666 /// println!("{}", response.text()?);
667 /// # Ok::<(), isahc::Error>(())
668 /// ```
669 #[inline]
670 pub fn get<U>(&self, uri: U) -> Result<Response<Body>, Error>
671 where
672 http::Uri: TryFrom<U>,
673 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
674 {
675 match http::Request::get(uri).body(()) {
676 Ok(request) => self.send(request),
677 Err(e) => Err(Error::from_any(e)),
678 }
679 }
680
681 /// Send a GET request to the given URI asynchronously.
682 ///
683 /// To customize the request further, see [`HttpClient::send_async`]. To
684 /// execute the request synchronously, see [`HttpClient::get`].
685 pub fn get_async<U>(&self, uri: U) -> ResponseFuture<'_>
686 where
687 http::Uri: TryFrom<U>,
688 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
689 {
690 match http::Request::get(uri).body(()) {
691 Ok(request) => self.send_async(request),
692 Err(e) => ResponseFuture::error(Error::from_any(e)),
693 }
694 }
695
696 /// Send a HEAD request to the given URI.
697 ///
698 /// To customize the request further, see [`HttpClient::send`]. To execute
699 /// the request asynchronously, see [`HttpClient::head_async`].
700 ///
701 /// # Examples
702 ///
703 /// ```no_run
704 /// use isahc::{prelude::*, HttpClient};
705 ///
706 /// let client = HttpClient::new()?;
707 /// let response = client.head("https://example.org")?;
708 /// println!("Page size: {:?}", response.headers()["content-length"]);
709 /// # Ok::<(), isahc::Error>(())
710 /// ```
711 #[inline]
712 pub fn head<U>(&self, uri: U) -> Result<Response<Body>, Error>
713 where
714 http::Uri: TryFrom<U>,
715 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
716 {
717 match http::Request::head(uri).body(()) {
718 Ok(request) => self.send(request),
719 Err(e) => Err(Error::from_any(e)),
720 }
721 }
722
723 /// Send a HEAD request to the given URI asynchronously.
724 ///
725 /// To customize the request further, see [`HttpClient::send_async`]. To
726 /// execute the request synchronously, see [`HttpClient::head`].
727 pub fn head_async<U>(&self, uri: U) -> ResponseFuture<'_>
728 where
729 http::Uri: TryFrom<U>,
730 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
731 {
732 match http::Request::head(uri).body(()) {
733 Ok(request) => self.send_async(request),
734 Err(e) => ResponseFuture::error(Error::from_any(e)),
735 }
736 }
737
738 /// Send a POST request to the given URI with a given request body.
739 ///
740 /// To customize the request further, see [`HttpClient::send`]. To execute
741 /// the request asynchronously, see [`HttpClient::post_async`].
742 ///
743 /// # Examples
744 ///
745 /// ```no_run
746 /// use isahc::{prelude::*, HttpClient};
747 ///
748 /// let client = HttpClient::new()?;
749 ///
750 /// let response = client.post("https://httpbin.org/post", r#"{
751 /// "speed": "fast",
752 /// "cool_name": true
753 /// }"#)?;
754 /// # Ok::<(), isahc::Error>(())
755 #[inline]
756 pub fn post<U, B>(&self, uri: U, body: B) -> Result<Response<Body>, Error>
757 where
758 http::Uri: TryFrom<U>,
759 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
760 B: Into<Body>,
761 {
762 match http::Request::post(uri).body(body) {
763 Ok(request) => self.send(request),
764 Err(e) => Err(Error::from_any(e)),
765 }
766 }
767
768 /// Send a POST request to the given URI asynchronously with a given request
769 /// body.
770 ///
771 /// To customize the request further, see [`HttpClient::send_async`]. To
772 /// execute the request synchronously, see [`HttpClient::post`].
773 pub fn post_async<U, B>(&self, uri: U, body: B) -> ResponseFuture<'_>
774 where
775 http::Uri: TryFrom<U>,
776 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
777 B: Into<AsyncBody>,
778 {
779 match http::Request::post(uri).body(body) {
780 Ok(request) => self.send_async(request),
781 Err(e) => ResponseFuture::error(Error::from_any(e)),
782 }
783 }
784
785 /// Send a PUT request to the given URI with a given request body.
786 ///
787 /// To customize the request further, see [`HttpClient::send`]. To execute
788 /// the request asynchronously, see [`HttpClient::put_async`].
789 ///
790 /// # Examples
791 ///
792 /// ```no_run
793 /// use isahc::{prelude::*, HttpClient};
794 ///
795 /// let client = HttpClient::new()?;
796 ///
797 /// let response = client.put("https://httpbin.org/put", r#"{
798 /// "speed": "fast",
799 /// "cool_name": true
800 /// }"#)?;
801 /// # Ok::<(), isahc::Error>(())
802 /// ```
803 #[inline]
804 pub fn put<U, B>(&self, uri: U, body: B) -> Result<Response<Body>, Error>
805 where
806 http::Uri: TryFrom<U>,
807 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
808 B: Into<Body>,
809 {
810 match http::Request::put(uri).body(body) {
811 Ok(request) => self.send(request),
812 Err(e) => Err(Error::from_any(e)),
813 }
814 }
815
816 /// Send a PUT request to the given URI asynchronously with a given request
817 /// body.
818 ///
819 /// To customize the request further, see [`HttpClient::send_async`]. To
820 /// execute the request synchronously, see [`HttpClient::put`].
821 pub fn put_async<U, B>(&self, uri: U, body: B) -> ResponseFuture<'_>
822 where
823 http::Uri: TryFrom<U>,
824 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
825 B: Into<AsyncBody>,
826 {
827 match http::Request::put(uri).body(body) {
828 Ok(request) => self.send_async(request),
829 Err(e) => ResponseFuture::error(Error::from_any(e)),
830 }
831 }
832
833 /// Send a DELETE request to the given URI.
834 ///
835 /// To customize the request further, see [`HttpClient::send`]. To execute
836 /// the request asynchronously, see [`HttpClient::delete_async`].
837 #[inline]
838 pub fn delete<U>(&self, uri: U) -> Result<Response<Body>, Error>
839 where
840 http::Uri: TryFrom<U>,
841 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
842 {
843 match http::Request::delete(uri).body(()) {
844 Ok(request) => self.send(request),
845 Err(e) => Err(Error::from_any(e)),
846 }
847 }
848
849 /// Send a DELETE request to the given URI asynchronously.
850 ///
851 /// To customize the request further, see [`HttpClient::send_async`]. To
852 /// execute the request synchronously, see [`HttpClient::delete`].
853 pub fn delete_async<U>(&self, uri: U) -> ResponseFuture<'_>
854 where
855 http::Uri: TryFrom<U>,
856 <http::Uri as TryFrom<U>>::Error: Into<http::Error>,
857 {
858 match http::Request::delete(uri).body(()) {
859 Ok(request) => self.send_async(request),
860 Err(e) => ResponseFuture::error(Error::from_any(e)),
861 }
862 }
863
864 /// Send an HTTP request and return the HTTP response.
865 ///
866 /// Upon success, will return a [`Response`] containing the status code,
867 /// response headers, and response body from the server. The [`Response`] is
868 /// returned as soon as the HTTP response headers are received; the
869 /// connection will remain open to stream the response body in real time.
870 /// Dropping the response body without fully consuming it will close the
871 /// connection early without downloading the rest of the response body.
872 ///
873 /// The response body is provided as a stream that may only be consumed
874 /// once. If you need to inspect the response body more than once, you will
875 /// have to either read it into memory or write it to a file.
876 ///
877 /// The response body is not a direct stream from the server, but uses its
878 /// own buffering mechanisms internally for performance. It is therefore
879 /// undesirable to wrap the body in additional buffering readers.
880 ///
881 /// _Note that the actual underlying socket connection isn't necessarily
882 /// closed on drop. It may remain open to be reused if pipelining is being
883 /// used, the connection is configured as `keep-alive`, and so on._
884 ///
885 /// This client's configuration can be overridden for this request by
886 /// configuring the request using methods provided by the [`Configurable`]
887 /// trait.
888 ///
889 /// To execute a request asynchronously, see [`HttpClient::send_async`].
890 ///
891 /// # Examples
892 ///
893 /// ```no_run
894 /// use isahc::{prelude::*, HttpClient, Request};
895 ///
896 /// let client = HttpClient::new()?;
897 ///
898 /// let request = Request::post("https://httpbin.org/post")
899 /// .header("Content-Type", "application/json")
900 /// .body(r#"{
901 /// "speed": "fast",
902 /// "cool_name": true
903 /// }"#)?;
904 ///
905 /// let response = client.send(request)?;
906 /// assert!(response.status().is_success());
907 /// # Ok::<(), isahc::Error>(())
908 /// ```
909 pub fn send<B>(&self, request: Request<B>) -> Result<Response<Body>, Error>
910 where
911 B: Into<Body>,
912 {
913 let span = tracing::debug_span!(
914 "send",
915 method = ?request.method(),
916 uri = ?request.uri(),
917 );
918
919 let mut writer_maybe = None;
920
921 let request = request.map(|body| {
922 let (async_body, writer) = body.into().into_async();
923 writer_maybe = writer;
924 async_body
925 });
926
927 let response = block_on(
928 async move {
929 // Instead of simply blocking the current thread until the response
930 // is received, we can use the current thread to read from the
931 // request body synchronously while concurrently waiting for the
932 // response.
933 if let Some(mut writer) = writer_maybe {
934 // Note that the `send_async` future is given first; this
935 // ensures that it is polled first and thus the request is
936 // initiated before we attempt to write the request body.
937 let (response, _) = try_zip(self.send_async_inner(request), async move {
938 writer.write().await.map_err(Error::from)
939 })
940 .await?;
941
942 Ok(response)
943 } else {
944 self.send_async_inner(request).await
945 }
946 }
947 .instrument(span),
948 )?;
949
950 Ok(response.map(|body| body.into_sync()))
951 }
952
953 /// Send an HTTP request and return the HTTP response asynchronously.
954 ///
955 /// Upon success, will return a [`Response`] containing the status code,
956 /// response headers, and response body from the server. The [`Response`] is
957 /// returned as soon as the HTTP response headers are received; the
958 /// connection will remain open to stream the response body in real time.
959 /// Dropping the response body without fully consuming it will close the
960 /// connection early without downloading the rest of the response body.
961 ///
962 /// The response body is provided as a stream that may only be consumed
963 /// once. If you need to inspect the response body more than once, you will
964 /// have to either read it into memory or write it to a file.
965 ///
966 /// The response body is not a direct stream from the server, but uses its
967 /// own buffering mechanisms internally for performance. It is therefore
968 /// undesirable to wrap the body in additional buffering readers.
969 ///
970 /// _Note that the actual underlying socket connection isn't necessarily
971 /// closed on drop. It may remain open to be reused if pipelining is being
972 /// used, the connection is configured as `keep-alive`, and so on._
973 ///
974 /// This client's configuration can be overridden for this request by
975 /// configuring the request using methods provided by the [`Configurable`]
976 /// trait.
977 ///
978 /// To execute a request synchronously, see [`HttpClient::send`].
979 ///
980 /// # Examples
981 ///
982 /// ```no_run
983 /// # async fn run() -> Result<(), isahc::Error> {
984 /// use isahc::{prelude::*, HttpClient, Request};
985 ///
986 /// let client = HttpClient::new()?;
987 ///
988 /// let request = Request::post("https://httpbin.org/post")
989 /// .header("Content-Type", "application/json")
990 /// .body(r#"{
991 /// "speed": "fast",
992 /// "cool_name": true
993 /// }"#)?;
994 ///
995 /// let response = client.send_async(request).await?;
996 /// assert!(response.status().is_success());
997 /// # Ok(()) }
998 /// ```
999 #[inline]
1000 pub fn send_async<B>(&self, request: Request<B>) -> ResponseFuture<'_>
1001 where
1002 B: Into<AsyncBody>,
1003 {
1004 let span = tracing::debug_span!(
1005 "send_async",
1006 method = ?request.method(),
1007 uri = ?request.uri(),
1008 );
1009
1010 ResponseFuture::new(
1011 self.send_async_inner(request.map(Into::into))
1012 .instrument(span),
1013 )
1014 }
1015
1016 /// Actually send the request. All the public methods go through here.
1017 async fn send_async_inner(
1018 &self,
1019 mut request: Request<AsyncBody>,
1020 ) -> Result<Response<AsyncBody>, Error> {
1021 // Populate request config, creating if necessary.
1022 if let Some(config) = request.extensions_mut().get_mut::<RequestConfig>() {
1023 // Merge request configuration with defaults.
1024 config.merge(&self.inner.request_config);
1025 } else {
1026 request
1027 .extensions_mut()
1028 .insert(self.inner.request_config.clone());
1029 }
1030
1031 let ctx = interceptor::Context {
1032 invoker: Arc::new(self),
1033 interceptors: &self.inner.interceptors,
1034 };
1035
1036 ctx.send(request).await
1037 }
1038
1039 fn create_easy_handle(
1040 &self,
1041 mut request: Request<AsyncBody>,
1042 ) -> Result<
1043 (
1044 curl::easy::Easy2<RequestHandler>,
1045 impl Future<Output = Result<Response<ResponseBodyReader>, Error>>,
1046 ),
1047 curl::Error,
1048 > {
1049 // Prepare the request plumbing.
1050 let body = std::mem::take(request.body_mut());
1051 let has_body = !body.is_empty();
1052 let body_length = body.len();
1053 let (handler, future) = RequestHandler::new(body);
1054
1055 let mut easy = curl::easy::Easy2::new(handler);
1056
1057 // Set whether curl should generate verbose debug data for us to log.
1058 easy.verbose(easy.get_ref().is_debug_enabled())?;
1059
1060 // Disable connection reuse logs if connection cache is disabled.
1061 if self.inner.client_config.close_connections {
1062 easy.get_mut().disable_connection_reuse_log = true;
1063 }
1064
1065 easy.signal(false)?;
1066
1067 let request_config = request
1068 .extensions()
1069 .get::<RequestConfig>()
1070 .unwrap();
1071
1072 request_config.set_opt(&mut easy)?;
1073 self.inner.client_config.set_opt(&mut easy)?;
1074
1075 // Check if we need to disable the Expect header.
1076 let disable_expect_header = request_config.expect_continue
1077 .as_ref()
1078 .map(|x| x.is_disabled())
1079 .unwrap_or_default();
1080
1081 // Set the HTTP method to use. Curl ties in behavior with the request
1082 // method, so we need to configure this carefully.
1083 #[allow(indirect_structural_match)]
1084 match (request.method(), has_body) {
1085 // Normal GET request.
1086 (&http::Method::GET, false) => {
1087 easy.get(true)?;
1088 }
1089 // Normal HEAD request.
1090 (&http::Method::HEAD, false) => {
1091 easy.nobody(true)?;
1092 }
1093 // POST requests have special redirect behavior.
1094 (&http::Method::POST, _) => {
1095 easy.post(true)?;
1096 }
1097 // Normal PUT request.
1098 (&http::Method::PUT, _) => {
1099 easy.upload(true)?;
1100 }
1101 // Default case is to either treat request like a GET or PUT.
1102 (method, has_body) => {
1103 easy.upload(has_body)?;
1104 easy.custom_request(method.as_str())?;
1105 }
1106 }
1107
1108 easy.url(&uri_to_string(request.uri()))?;
1109
1110 // If the request has a body, then we either need to tell curl how large
1111 // the body is if we know it, or tell curl to use chunked encoding. If
1112 // we do neither, curl will simply not send the body without warning.
1113 if has_body {
1114 // Use length given in Content-Length header, or the size defined by
1115 // the body itself.
1116 let body_length = request
1117 .headers()
1118 .get("Content-Length")
1119 .and_then(|value| value.to_str().ok())
1120 .and_then(|value| value.parse().ok())
1121 .or(body_length);
1122
1123 if let Some(len) = body_length {
1124 if request.method() == http::Method::POST {
1125 easy.post_field_size(len)?;
1126 } else {
1127 easy.in_filesize(len)?;
1128 }
1129 } else {
1130 // Set the Transfer-Encoding header to instruct curl to use
1131 // chunked encoding. Replaces any existing values that may be
1132 // incorrect.
1133 request.headers_mut().insert(
1134 "Transfer-Encoding",
1135 http::header::HeaderValue::from_static("chunked"),
1136 );
1137 }
1138 }
1139
1140 // Generate a header list for curl.
1141 let mut headers = curl::easy::List::new();
1142
1143 let title_case = request
1144 .extensions()
1145 .get::<RequestConfig>()
1146 .unwrap()
1147 .title_case_headers
1148 .unwrap_or(false);
1149
1150 for (name, value) in request.headers().iter() {
1151 headers.append(&header_to_curl_string(name, value, title_case))?;
1152 }
1153
1154 if disable_expect_header {
1155 headers.append("Expect:")?;
1156 }
1157
1158 easy.http_headers(headers)?;
1159
1160 Ok((easy, future))
1161 }
1162}
1163
1164impl crate::interceptor::Invoke for &HttpClient {
1165 fn invoke(
1166 &self,
1167 mut request: Request<AsyncBody>,
1168 ) -> crate::interceptor::InterceptorFuture<'_, Error> {
1169 Box::pin(async move {
1170 let is_head_request = request.method() == http::Method::HEAD;
1171
1172 // Set default user agent if not specified.
1173 request
1174 .headers_mut()
1175 .entry(http::header::USER_AGENT)
1176 .or_insert(USER_AGENT.parse().unwrap());
1177
1178 // Check if automatic decompression is enabled; we'll need to know
1179 // this later after the response is sent.
1180 let is_automatic_decompression = request
1181 .extensions()
1182 .get::<RequestConfig>()
1183 .unwrap()
1184 .automatic_decompression
1185 .unwrap_or(false);
1186
1187 // Create and configure a curl easy handle to fulfil the request.
1188 let (easy, future) = self.create_easy_handle(request).map_err(Error::from_any)?;
1189
1190 // Send the request to the agent to be executed.
1191 self.inner.agent.submit_request(easy)?;
1192
1193 // Await for the response headers.
1194 let response = future.await?;
1195
1196 // If a Content-Length header is present, include that information in
1197 // the body as well.
1198 let body_len = response.content_length().filter(|_| {
1199 // If automatic decompression is enabled, and will likely be
1200 // selected, then the value of Content-Length does not indicate
1201 // the uncompressed body length and merely the compressed data
1202 // length. If it looks like we are in this scenario then we
1203 // ignore the Content-Length, since it can only cause confusion
1204 // when included with the body.
1205 if is_automatic_decompression {
1206 if let Some(value) = response.headers().get(http::header::CONTENT_ENCODING) {
1207 if value != "identity" {
1208 return false;
1209 }
1210 }
1211 }
1212
1213 true
1214 });
1215
1216 // Convert the reader into an opaque Body.
1217 Ok(response.map(|reader| {
1218 if is_head_request {
1219 AsyncBody::empty()
1220 } else {
1221 let body = ResponseBody {
1222 inner: reader,
1223 // Extend the lifetime of the agent by including a reference
1224 // to its handle in the response body.
1225 _client: (*self).clone(),
1226 };
1227
1228 if let Some(len) = body_len {
1229 AsyncBody::from_reader_sized(body, len)
1230 } else {
1231 AsyncBody::from_reader(body)
1232 }
1233 }
1234 }))
1235 })
1236 }
1237}
1238
1239impl fmt::Debug for HttpClient {
1240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241 f.debug_struct("HttpClient").finish()
1242 }
1243}
1244
1245/// A future for a request being executed.
1246#[must_use = "futures do nothing unless you `.await` or poll them"]
1247pub struct ResponseFuture<'c>(Pin<Box<dyn Future<Output = <Self as Future>::Output> + 'c + Send>>);
1248
1249impl<'c> ResponseFuture<'c> {
1250 fn new<F>(future: F) -> Self
1251 where
1252 F: Future<Output = <Self as Future>::Output> + Send + 'c,
1253 {
1254 ResponseFuture(Box::pin(future))
1255 }
1256
1257 fn error(error: Error) -> Self {
1258 Self::new(async move { Err(error) })
1259 }
1260}
1261
1262impl Future for ResponseFuture<'_> {
1263 type Output = Result<Response<AsyncBody>, Error>;
1264
1265 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1266 self.0.as_mut().poll(cx)
1267 }
1268}
1269
1270impl<'c> fmt::Debug for ResponseFuture<'c> {
1271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1272 f.debug_struct("ResponseFuture").finish()
1273 }
1274}
1275
1276/// Response body stream. Holds a reference to the agent to ensure it is kept
1277/// alive until at least this transfer is complete.
1278struct ResponseBody {
1279 inner: ResponseBodyReader,
1280 _client: HttpClient,
1281}
1282
1283impl AsyncRead for ResponseBody {
1284 fn poll_read(
1285 mut self: Pin<&mut Self>,
1286 cx: &mut Context<'_>,
1287 buf: &mut [u8],
1288 ) -> Poll<io::Result<usize>> {
1289 let inner = Pin::new(&mut self.inner);
1290 inner.poll_read(cx, buf)
1291 }
1292}
1293
1294/// Convert a URI to a string. This implementation is a bit faster than the
1295/// `Display` implementation that avoids the `std::fmt` machinery.
1296fn uri_to_string(uri: &http::Uri) -> String {
1297 let mut s = String::new();
1298
1299 if let Some(scheme) = uri.scheme() {
1300 s.push_str(scheme.as_str());
1301 s.push_str("://");
1302 }
1303
1304 if let Some(authority) = uri.authority() {
1305 s.push_str(authority.as_str());
1306 }
1307
1308 s.push_str(uri.path());
1309
1310 if let Some(query) = uri.query() {
1311 s.push('?');
1312 s.push_str(query);
1313 }
1314
1315 s
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320 use super::*;
1321
1322 static_assertions::assert_impl_all!(HttpClient: Send, Sync);
1323 static_assertions::assert_impl_all!(HttpClientBuilder: Send);
1324
1325 #[test]
1326 fn test_default_header() {
1327 let client = HttpClientBuilder::new()
1328 .default_header("some-key", "some-value")
1329 .build();
1330 match client {
1331 Ok(_) => assert!(true),
1332 Err(_) => assert!(false),
1333 }
1334 }
1335
1336 #[test]
1337 fn test_default_headers_mut() {
1338 let mut builder = HttpClientBuilder::new().default_header("some-key", "some-value");
1339 let headers_map = &mut builder.default_headers;
1340 assert!(headers_map.len() == 1);
1341
1342 let mut builder = HttpClientBuilder::new()
1343 .default_header("some-key", "some-value1")
1344 .default_header("some-key", "some-value2");
1345 let headers_map = &mut builder.default_headers;
1346
1347 assert!(headers_map.len() == 2);
1348
1349 let mut builder = HttpClientBuilder::new();
1350 let header_map = &mut builder.default_headers;
1351 assert!(header_map.is_empty())
1352 }
1353}