Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow use of Expect header to be configured #340

Merged
merged 1 commit into from
Aug 22, 2021
Merged
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
16 changes: 13 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1063,14 +1063,20 @@ impl HttpClient {

easy.signal(false)?;

request
let request_config = request
.extensions()
.get::<RequestConfig>()
.unwrap()
.set_opt(&mut easy)?;
.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
.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)]
Expand Down Expand Up @@ -1144,6 +1150,10 @@ impl HttpClient {
headers.append(&header_to_curl_string(name, value, title_case))?;
}

if disable_expect_header {
headers.append("Expect:")?;
}

easy.http_headers(headers)?;

Ok((easy, future))
Expand Down
151 changes: 151 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,73 @@ pub trait Configurable: request::WithRequestConfig {
})
}

/// Configure the use of the `Expect` request header when sending request
/// bodies with HTTP/1.1.
///
/// By default, when sending requests containing a body of large or unknown
/// length over HTTP/1.1, Isahc will send the request headers first without
/// the body and wait for the server to respond with a 100 (Continue) status
/// code, as defined by [RFC 7231, Section
/// 5.1.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1).
/// This gives the opportunity for the server to reject the response without
/// needing to first transmit the request body over the network, if the body
/// contents are not necessary for the server to determine an appropriate
/// response.
///
/// For servers that do not support this behavior and instead simply wait
/// for the request body without responding with a 100 (Continue), there is
/// a limited timeout before the response body is sent anyway without
/// confirmation. The default timeout is 1 second, but this can be
/// configured.
///
/// The `Expect` behavior can also be disabled entirely.
///
/// This configuration only takes effect when using HTTP/1.1.
///
/// # Examples
///
/// ```
/// use std::time::Duration;
/// use isahc::{
/// config::ExpectContinue,
/// prelude::*,
/// HttpClient,
/// };
///
/// // Use the default behavior (enabled).
/// let client = HttpClient::builder()
/// .expect_continue(ExpectContinue::enabled())
/// // or equivalently...
/// .expect_continue(ExpectContinue::default())
/// // or equivalently...
/// .expect_continue(true)
/// .build()?;
///
/// // Customize the timeout if the server doesn't respond with a 100
/// // (Continue) status.
/// let client = HttpClient::builder()
/// .expect_continue(ExpectContinue::timeout(Duration::from_millis(200)))
/// // or equivalently...
/// .expect_continue(Duration::from_millis(200))
/// .build()?;
///
/// // Disable the Expect header entirely.
/// let client = HttpClient::builder()
/// .expect_continue(ExpectContinue::disabled())
/// // or equivalently...
/// .expect_continue(false)
/// .build()?;
/// # Ok::<(), isahc::Error>(())
/// ```
fn expect_continue<T>(self, expect: T) -> Self
where
T: Into<ExpectContinue>,
{
self.with_config(move |config| {
config.expect_continue = Some(expect.into());
})
}

/// Set one or more default HTTP authentication methods to attempt to use
/// when authenticating with the server.
///
Expand Down Expand Up @@ -872,3 +939,87 @@ impl SetOpt for IpVersion {
})
}
}

/// Controls the use of the `Expect` request header when sending request bodies
/// with HTTP/1.1.
///
/// By default, when sending requests containing a body of large or unknown
/// length over HTTP/1.1, Isahc will send the request headers first without the
/// body and wait for the server to respond with a 100 (Continue) status code,
/// as defined by [RFC 7231, Section
/// 5.1.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1). This
/// gives the opportunity for the server to reject the response without needing
/// to first transmit the request body over the network, if the body contents
/// are not necessary for the server to determine an appropriate response.
///
/// For servers that do not support this behavior and instead simply wait for
/// the request body without responding with a 100 (Continue), there is a
/// limited timeout before the response body is sent anyway without
/// confirmation. The default timeout is 1 second, but this can be configured.
///
/// The `Expect` behavior can also be disabled entirely.
///
/// This configuration only takes effect when using HTTP/1.1.
#[derive(Clone, Debug)]
pub struct ExpectContinue {
timeout: Option<Duration>,
}

impl ExpectContinue {
/// Enable the use of `Expect` and wait for a 100 (Continue) response with a
/// default timeout before sending a request body.
pub const fn enabled() -> Self {
Self::timeout(Duration::from_secs(1))
}

/// Enable the use of `Expect` and wait for a 100 (Continue) response, up to
/// the given timeout, before sending a request body.
pub const fn timeout(timeout: Duration) -> Self {
Self {
timeout: Some(timeout),
}
}

/// Disable the use and handling of the `Expect` request header.
pub const fn disabled() -> Self {
Self {
timeout: None,
}
}

pub(crate) fn is_disabled(&self) -> bool {
self.timeout.is_none()
}
}

impl Default for ExpectContinue {
fn default() -> Self {
Self::enabled()
}
}

impl From<bool> for ExpectContinue {
fn from(value: bool) -> Self {
if value {
Self::enabled()
} else {
Self::disabled()
}
}
}

impl From<Duration> for ExpectContinue {
fn from(value: Duration) -> Self {
Self::timeout(value)
}
}

impl SetOpt for ExpectContinue {
fn set_opt<H>(&self, easy: &mut Easy2<H>) -> Result<(), curl::Error> {
if let Some(timeout) = self.timeout {
easy.expect_100_timeout(timeout)
} else {
Ok(())
}
}
}
5 changes: 5 additions & 0 deletions src/config/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ define_request_config! {
low_speed_timeout: Option<(u32, Duration)>,
version_negotiation: Option<VersionNegotiation>,
automatic_decompression: Option<bool>,
expect_continue: Option<ExpectContinue>,
authentication: Option<Authentication>,
credentials: Option<Credentials>,
tcp_keepalive: Option<Duration>,
Expand Down Expand Up @@ -136,6 +137,10 @@ impl SetOpt for RequestConfig {
}
}

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

if let Some(auth) = self.authentication.as_ref() {
auth.set_opt(easy)?;
}
Expand Down
29 changes: 29 additions & 0 deletions tests/expect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use isahc::{Body, Request, prelude::*};
use testserver::mock;

#[test]
fn expect_header_is_sent_by_default() {
let m = mock!();

let body = Body::from_reader("hello world".as_bytes());

isahc::post(m.url(), body).unwrap();

m.request().expect_header("expect", "100-continue");
}

#[test]
fn expect_header_is_not_sent_when_disabled() {
let m = mock!();

let body = Body::from_reader("hello world".as_bytes());

Request::post(m.url())
.expect_continue(false)
.body(body)
.unwrap()
.send()
.unwrap();

assert!(m.request().get_header("expect").next().is_none());
}