Skip to content

Commit

Permalink
Support sending informational (1xx) responses
Browse files Browse the repository at this point in the history
Support for `Expect: 100-continue` is mandated as MUST by
https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1. Yet servers
built on `h2` cannot currently support this requirement.

One example of such usage, is [hyper #2743](hyperium/hyper#2743).

This approach adds a `send_info` method to `SendResponse` that a server
application can use to implement support itself. This PR does _not_ solve
the feature itself, it merely provides sufficient support for a server
application to implement the functionality as desired.
  • Loading branch information
rawler committed Oct 13, 2024
1 parent 77be664 commit 6591cea
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/proto/streams/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ impl State {
Open { local, remote }
}
}
HalfClosedRemote(AwaitingHeaders) | ReservedLocal => {
HalfClosedRemote(AwaitingHeaders | Streaming) | ReservedLocal => {
if eos {
Closed(Cause::EndStream)
} else {
Expand Down
20 changes: 20 additions & 0 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,26 @@ impl<B: Buf> SendResponse<B> {
.map_err(Into::into)
}

/// Send a non-final 1xx response to a client request.
///
/// The [`SendResponse`] instance is already associated with a received
/// request. This function may only be called if [`send_reset`] or
/// [`send_response`] has not been previously called.
///
/// [`SendResponse`]: #
/// [`send_reset`]: #method.send_reset
/// [`send_response`]: #method.send_response
///
/// # Panics
///
/// If a "final" response has already been sent, or if the stream has been reset.
pub fn send_info(&mut self, response: Response<()>) -> Result<(), crate::Error> {
assert!(response.status().is_informational());
self.inner
.send_response(response, false)
.map_err(Into::into)
}

/// Push a request and response to the client
///
/// On success, a [`SendResponse`] instance is returned.
Expand Down
55 changes: 55 additions & 0 deletions tests/h2-tests/tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,61 @@ async fn serve_request() {
join(client, srv).await;
}

#[tokio::test]
async fn serve_request_expect_continue() {
h2_support::trace_init!();
let (io, mut client) = mock::new();

let client = async move {
let settings = client.assert_server_handshake().await;
assert_default_settings!(settings);
client
.send_frame(
frames::headers(1)
.field(http::header::EXPECT, "100-continue")
.request("POST", "https://example.com/"),
)
.await;
client.recv_frame(frames::headers(1).response(100)).await;
client
.send_frame(frames::data(1, "hello world").eos())
.await;
client
.recv_frame(frames::headers(1).response(200).eos())
.await;
};

let srv = async move {
let mut srv = server::handshake(io).await.expect("handshake");
let (req, mut stream) = srv.next().await.unwrap().unwrap();

assert_eq!(req.method(), &http::Method::POST);
assert_eq!(
req.headers().get(http::header::EXPECT),
Some(&http::HeaderValue::from_static("100-continue"))
);

let connection_fut = poll_fn(|cx| srv.poll_closed(cx).map(Result::ok));
let test_fut = async move {
stream.send_continue().unwrap();

let mut body = req.into_body();
assert_eq!(
body.next().await.unwrap().unwrap(),
Bytes::from_static(b"hello world")
);
assert!(body.next().await.is_none());

let rsp = http::Response::builder().status(200).body(()).unwrap();
stream.send_response(rsp, true).unwrap();
};

join(connection_fut, test_fut).await;
};

join(client, srv).await;
}

#[tokio::test]
async fn serve_connect() {
h2_support::trace_init!();
Expand Down

0 comments on commit 6591cea

Please sign in to comment.