Skip to content

Commit

Permalink
Allow use of Expect header to be configured (#340)
Browse files Browse the repository at this point in the history
Add a configuration option for changing how POST requests behave under HTTP/1.1 and the `Expect: 100-continue` request behavior. Allow the user to change the timeout to a different duration or to disable the header entirely.

Replaces #311.

Fixes #303.
  • Loading branch information
sagebind authored Aug 22, 2021
1 parent 2a5c175 commit d71fea6
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 3 deletions.
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());
}

0 comments on commit d71fea6

Please sign in to comment.