Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,14 @@ features = ["std", "std-future"]
env_logger = "0.9"
flate2 = "1.0.3"
indicatif = "0.15"
libc = "0.2.177"
rayon = "1"
serde_json = "1"
static_assertions = "1.1"
structopt = "0.3"
tempfile = "3.1"
test-case = "2.0"
tracing-subscriber = ">=0.2.12, <0.4.0"
serde_json = "1"

[dev-dependencies.testserver]
path = "testserver"
Expand Down
4 changes: 4 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use std::{env, error::Error};
fn main() -> Result<(), Box<dyn Error>> {
println!("cargo:rustc-env=ISAHC_FEATURES={}", get_feature_string());

// Allow conditional compilation for tarpaulin and debug builds.
println!("cargo::rustc-check-cfg=cfg(tarpaulin)");
println!("cargo::rustc-check-cfg=cfg(debug)");

Ok(())
}

Expand Down
138 changes: 107 additions & 31 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
body::{AsyncBody, Body},
config::{
client::ClientConfig,
request::{RequestConfig, SetOpt, WithRequestConfig},
request::{NoCustomOpt, RequestConfig, SetOpt, WithRequestConfig},
*,
},
default_headers::DefaultHeadersInterceptor,
Expand Down Expand Up @@ -71,9 +71,9 @@ static USER_AGENT: Lazy<String> = Lazy::new(|| {
/// # Ok::<(), isahc::Error>(())
/// ```
#[must_use = "builders have no effect if unused"]
pub struct HttpClientBuilder {
pub struct HttpClientBuilder<T: SetOpt> {
agent_builder: AgentBuilder,
client_config: ClientConfig,
client_config: ClientConfig<T>,
request_config: RequestConfig,
interceptors: Vec<InterceptorObj>,
default_headers: HeaderMap<HeaderValue>,
Expand All @@ -83,13 +83,13 @@ pub struct HttpClientBuilder {
cookie_jar: Option<crate::cookies::CookieJar>,
}

impl Default for HttpClientBuilder {
impl Default for HttpClientBuilder<NoCustomOpt> {
fn default() -> Self {
Self::new()
}
}

impl HttpClientBuilder {
impl HttpClientBuilder<NoCustomOpt> {
/// Create a new builder for building a custom client. All configuration
/// will start out with the default values.
///
Expand All @@ -114,6 +114,75 @@ impl HttpClientBuilder {
}
}

/// Set custom curl options.
///
/// # Examples
///
/// ```
/// use std::ptr;
/// use std::error;
/// use std::fmt;
/// use std::mem;
/// use std::ffi;
///
/// use isahc::{HttpClient, SetOpt, prelude::*};
///
/// /// Helper functions for raising nice errors taken from the `curl` crate.
/// fn cvt<H>(easy: &mut curl::easy::Easy2<H>, rc: curl_sys::CURLcode) -> Result<(), curl::Error> {
/// if rc == curl_sys::CURLE_OK {
/// return Ok(());
/// }
/// let mut err = curl::Error::new(rc);
/// if let Some(msg) = easy.take_error_buf() {
/// err.set_extra(msg);
/// }
/// Err(err)
/// }
///
/// /// Import the base function for the open socket callback from the `curl` crate.
/// extern "C" {
/// pub fn opensocket_cb(data: *mut libc::c_void, purpose: curl_sys::curlsocktype, address: *mut curl_sys::curl_sockaddr) -> curl_sys::curl_socket_t;
/// }
///
/// /// Wrap the callback to perform our custom logic before opening the socket.
/// extern "C" fn opensocket_cb_custom(data: *mut libc::c_void, purpose: curl_sys::curlsocktype, address: *mut curl_sys::curl_sockaddr) -> curl_sys::curl_socket_t {
/// // TODO: Wrapper the open socket callback to perform some custom logic.
/// unsafe { opensocket_cb(data, purpose, address) }
/// }
///
/// /// Implement a custom curl option that modifies the open socket function.
/// #[derive(Debug)]
/// struct CustomOpt {}
/// impl SetOpt for CustomOpt {
/// fn set_opt<H>(&self, easy: &mut curl::easy::Easy2<H>) -> Result<(), curl::Error> {
/// let opt = curl_sys::CURLOPT_OPENSOCKETFUNCTION;
/// let cb: curl_sys::curl_opensocket_callback = opensocket_cb_custom;
/// unsafe { cvt(easy, curl_sys::curl_easy_setopt(easy.raw(), opt, cb))?; }
/// Ok(())
/// }
/// }
///
/// let client = HttpClient::builder()
/// .custom_curl_options(CustomOpt {})
/// .build()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn custom_curl_options<S: SetOpt>(self, custom_curl_options: S) -> HttpClientBuilder<S> {
// Similar to the dns_cache option, this operation actually affects all
// requests in a multi handle so we do not expose it per-request to
// avoid confusing behavior.
HttpClientBuilder{
agent_builder: self.agent_builder,
client_config: ClientConfig { connection_cache_ttl: self.client_config.connection_cache_ttl, close_connections: self.client_config.close_connections, dns_cache: self.client_config.dns_cache, dns_resolve: self.client_config.dns_resolve, custom_curl_options: Some(custom_curl_options), },
request_config: self.request_config,
interceptors: self.interceptors,
default_headers: self.default_headers,
error: self.error,
}
}
}

impl<T: SetOpt> HttpClientBuilder<T> {
/// Enable persistent cookie handling for all requests using this client
/// using a shared cookie jar.
///
Expand Down Expand Up @@ -436,7 +505,7 @@ impl HttpClientBuilder {
///
/// If the client fails to initialize, an error will be returned.
#[allow(unused_mut)]
pub fn build(mut self) -> Result<HttpClient, Error> {
pub fn build(mut self) -> Result<HttpClient<T>, Error> {
if let Some(err) = self.error {
return Err(err);
}
Expand Down Expand Up @@ -483,23 +552,23 @@ impl HttpClientBuilder {
}
}

impl Configurable for HttpClientBuilder {
impl<T: SetOpt> Configurable for HttpClientBuilder<T> {
#[cfg(feature = "cookies")]
fn cookie_jar(mut self, cookie_jar: crate::cookies::CookieJar) -> Self {
self.cookie_jar = Some(cookie_jar);
self
}
}

impl WithRequestConfig for HttpClientBuilder {
impl<T: SetOpt> WithRequestConfig for HttpClientBuilder<T> {
#[inline]
fn with_config(mut self, f: impl FnOnce(&mut RequestConfig)) -> Self {
f(&mut self.request_config);
self
}
}

impl fmt::Debug for HttpClientBuilder {
impl<T: SetOpt> fmt::Debug for HttpClientBuilder<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClientBuilder").finish()
}
Expand Down Expand Up @@ -594,17 +663,24 @@ impl<'a, K: Copy, V: Copy> HeaderPair<K, V> for &'a (K, V) {
///
/// See the documentation on [`HttpClientBuilder`] for a comprehensive look at
/// what can be configured.
#[derive(Clone)]
pub struct HttpClient {
inner: Arc<Inner>,
pub struct HttpClient<T: SetOpt> {
inner: Arc<Inner<T>>,
}

struct Inner {
impl<T: SetOpt> Clone for HttpClient<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}

struct Inner<T: SetOpt> {
/// This is how we talk to our background agent thread.
agent: agent::Handle,

/// Client-wide request configuration.
client_config: ClientConfig,
client_config: ClientConfig<T>,

/// Default request configuration to use if not specified in a request.
request_config: RequestConfig,
Expand All @@ -617,7 +693,7 @@ struct Inner {
cookie_jar: Option<crate::cookies::CookieJar>,
}

impl HttpClient {
impl HttpClient<NoCustomOpt> {
/// Create a new HTTP client using the default configuration.
///
/// If the client fails to initialize, an error will be returned.
Expand All @@ -629,17 +705,19 @@ impl HttpClient {
///
/// TODO: Stabilize.
pub(crate) fn shared() -> &'static Self {
static SHARED: Lazy<HttpClient> =
static SHARED: Lazy<HttpClient<NoCustomOpt>> =
Lazy::new(|| HttpClient::new().expect("shared client failed to initialize"));

&SHARED
}

/// Create a new [`HttpClientBuilder`] for building a custom client.
pub fn builder() -> HttpClientBuilder {
pub fn builder() -> HttpClientBuilder<NoCustomOpt> {
HttpClientBuilder::default()
}
}

impl<T: SetOpt + 'static> HttpClient<T> {
/// Get the configured cookie jar for this HTTP client, if any.
///
/// # Availability
Expand Down Expand Up @@ -1064,23 +1142,20 @@ impl HttpClient {

easy.signal(false)?;

let request_config = request
.extensions()
.get::<RequestConfig>()
.unwrap();
let request_config = request.extensions().get::<RequestConfig>().unwrap();

request_config.set_opt(&mut easy)?;
self.inner.client_config.set_opt(&mut easy)?;

// Check if we need to disable the Expect header.
let disable_expect_header = request_config.expect_continue
let disable_expect_header = request_config
.expect_continue
.as_ref()
.map(|x| x.is_disabled())
.unwrap_or_default();

// Set the HTTP method to use. Curl ties in behavior with the request
// method, so we need to configure this carefully.
#[allow(indirect_structural_match)]
match (request.method(), has_body) {
// Normal GET request.
(&http::Method::GET, false) => {
Expand Down Expand Up @@ -1161,7 +1236,7 @@ impl HttpClient {
}
}

impl crate::interceptor::Invoke for &HttpClient {
impl<T: SetOpt + 'static> crate::interceptor::Invoke for &HttpClient<T> {
fn invoke(
&self,
mut request: Request<AsyncBody>,
Expand Down Expand Up @@ -1236,7 +1311,7 @@ impl crate::interceptor::Invoke for &HttpClient {
}
}

impl fmt::Debug for HttpClient {
impl<T: SetOpt> fmt::Debug for HttpClient<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpClient").finish()
}
Expand Down Expand Up @@ -1275,12 +1350,12 @@ impl<'c> fmt::Debug for ResponseFuture<'c> {

/// Response body stream. Holds a reference to the agent to ensure it is kept
/// alive until at least this transfer is complete.
struct ResponseBody {
struct ResponseBody<T: SetOpt> {
inner: ResponseBodyReader,
_client: HttpClient,
_client: HttpClient<T>,
}

impl AsyncRead for ResponseBody {
impl<T: SetOpt> AsyncRead for ResponseBody<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
Expand Down Expand Up @@ -1319,8 +1394,8 @@ fn uri_to_string(uri: &http::Uri) -> String {
mod tests {
use super::*;

static_assertions::assert_impl_all!(HttpClient: Send, Sync);
static_assertions::assert_impl_all!(HttpClientBuilder: Send);
static_assertions::assert_impl_all!(HttpClient::<NoCustomOpt>: Send, Sync);
static_assertions::assert_impl_all!(HttpClientBuilder::<NoCustomOpt>: Send);

#[test]
fn test_default_header() {
Expand All @@ -1335,7 +1410,8 @@ mod tests {

#[test]
fn test_default_headers_mut() {
let mut builder = HttpClientBuilder::new().default_header("some-key", "some-value");
let mut builder =
HttpClientBuilder::new().default_header("some-key", "some-value");
let headers_map = &mut builder.default_headers;
assert!(headers_map.len() == 1);

Expand Down
27 changes: 23 additions & 4 deletions src/config/client.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,33 @@
use crate::NoCustomOpt;

use super::{
dns::{DnsCache, ResolveMap},
request::SetOpt,
};
use std::time::Duration;
use std::{fmt::Debug, time::Duration};

#[derive(Debug, Default)]
pub(crate) struct ClientConfig {
#[derive(Debug)]
pub(crate) struct ClientConfig<T: SetOpt> {
pub(crate) connection_cache_ttl: Option<Duration>,
pub(crate) close_connections: bool,
pub(crate) dns_cache: Option<DnsCache>,
pub(crate) dns_resolve: Option<ResolveMap>,
pub(crate) custom_curl_options: Option<T>,
}

impl SetOpt for ClientConfig {
impl Default for ClientConfig<NoCustomOpt> {
fn default() -> Self {
Self {
connection_cache_ttl: Default::default(),
close_connections: Default::default(),
dns_cache: Default::default(),
dns_resolve: Default::default(),
custom_curl_options: None,
}
}
}

impl<T: SetOpt> SetOpt for ClientConfig<T> {
fn set_opt<H>(&self, easy: &mut curl::easy::Easy2<H>) -> Result<(), curl::Error> {
if let Some(ttl) = self.connection_cache_ttl {
easy.maxage_conn(ttl)?;
Expand All @@ -26,6 +41,10 @@ impl SetOpt for ClientConfig {
map.set_opt(easy)?;
}

if let Some(custom_curl_options) = self.custom_curl_options.as_ref() {
custom_curl_options.set_opt(easy)?;
}

easy.forbid_reuse(self.close_connections)
}
}
3 changes: 2 additions & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// to update the client code to apply the option when configuring an easy
// handle.

use self::{proxy::Proxy, request::SetOpt};
use self::{proxy::Proxy};
use crate::{
auth::{Authentication, Credentials},
is_http_version_supported,
Expand All @@ -32,6 +32,7 @@ pub(crate) mod ssl;
pub use dial::{Dialer, DialerParseError};
pub use dns::{DnsCache, ResolveMap};
pub use redirect::RedirectPolicy;
pub use request::SetOpt;
pub use ssl::{CaCertificate, ClientCertificate, PrivateKey, SslOption};

/// Provides additional methods when building a request for configuring various
Expand Down
Loading