Skip to content

Commit 90fecb8

Browse files
author
Apu Islam
committed
feat(http2): implement HTTP/2 informational responses support
Add support for HTTP/2 informational responses (1xx status codes) including 103 Early Hints. This enables servers to send preliminary headers before the final response, improving client performance through early resource discovery and connection establishment. Changes include: - extend client and server APIs to handle interim informational responses - update stream state management for 1xx responses - add test for interim informational response scenarios - fix MSRV compatibility by updating hashbrown to 0.16.1 in CI.yml, and upgrading rustc to 1.65 in Cargo.toml This implementation follows RFC 7540 and RFC 8297 specifications for HTTP/2 informational responses handling.
1 parent b9d5397 commit 90fecb8

File tree

9 files changed

+668
-17
lines changed

9 files changed

+668
-17
lines changed

.github/workflows/CI.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ jobs:
9292
run: |
9393
cargo update --package tokio --precise 1.38.1
9494
cargo update --package tokio-util --precise 0.7.11
95-
cargo update --package hashbrown --precise 0.15.0
95+
cargo update --package indexmap --precise 2.6.0
96+
cargo update --package hashbrown --precise 0.15.1
9697
cargo update --package once_cell --precise 1.20.3
97-
cargo update --package tracing-core --precise 0.1.33
9898
9999
100100
- run: cargo check -p h2

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ keywords = ["http", "async", "non-blocking"]
1717
categories = ["asynchronous", "web-programming", "network-programming"]
1818
exclude = ["fixtures/**", "ci/**"]
1919
edition = "2021"
20-
rust-version = "1.63"
20+
rust-version = "1.65"
2121

2222
[features]
2323
# Enables `futures::Stream` implementations for various types.

src/client.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,6 +1485,22 @@ impl ResponseFuture {
14851485
pub fn stream_id(&self) -> crate::StreamId {
14861486
crate::StreamId::from_internal(self.inner.stream_id())
14871487
}
1488+
1489+
/// Polls for informational responses (1xx status codes).
1490+
///
1491+
/// This method should be called before polling the main response future
1492+
/// to check for any informational responses that have been received.
1493+
///
1494+
/// Returns `Poll::Ready(Some(response))` if an informational response is available,
1495+
/// `Poll::Ready(None)` if no more informational responses are expected,
1496+
/// or `Poll::Pending` if no informational response is currently available.
1497+
pub fn poll_informational(
1498+
&mut self,
1499+
cx: &mut Context<'_>,
1500+
) -> Poll<Option<Result<Response<()>, crate::Error>>> {
1501+
self.inner.poll_informational(cx).map_err(Into::into)
1502+
}
1503+
14881504
/// Returns a stream of PushPromises
14891505
///
14901506
/// # Panics

src/codec/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ pub enum UserError {
4949

5050
/// Tries to send push promise to peer who has disabled server push
5151
PeerDisabledServerPush,
52+
53+
/// Invalid status code for informational response (must be 1xx)
54+
InvalidInformationalStatusCode,
5255
}
5356

5457
// ===== impl SendError =====
@@ -97,6 +100,7 @@ impl fmt::Display for UserError {
97100
SendPingWhilePending => "send_ping before received previous pong",
98101
SendSettingsWhilePending => "sending SETTINGS before received previous ACK",
99102
PeerDisabledServerPush => "sending PUSH_PROMISE to peer who disabled server push",
103+
InvalidInformationalStatusCode => "invalid informational status code",
100104
})
101105
}
102106
}

src/proto/streams/recv.rs

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ pub(super) enum Event {
6666
Headers(peer::PollMessage),
6767
Data(Bytes),
6868
Trailers(HeaderMap),
69+
InformationalHeaders(peer::PollMessage),
6970
}
7071

7172
#[derive(Debug)]
@@ -264,6 +265,21 @@ impl Recv {
264265
// corresponding headers frame pushed to `stream.pending_recv`.
265266
self.pending_accept.push(stream);
266267
}
268+
} else {
269+
// This is an informational response (1xx status code)
270+
// Convert to response and store it for polling
271+
let message = counts
272+
.peer()
273+
.convert_poll_message(pseudo, fields, stream_id)?;
274+
275+
tracing::debug!("Received informational response: {:?}", message);
276+
277+
// Push the informational response onto the stream's recv buffer
278+
// with a special event type so it can be polled separately
279+
stream
280+
.pending_recv
281+
.push_back(&mut self.buffer, Event::InformationalHeaders(message));
282+
stream.notify_recv();
267283
}
268284

269285
Ok(())
@@ -324,24 +340,63 @@ impl Recv {
324340
) -> Poll<Result<Response<()>, proto::Error>> {
325341
use super::peer::PollMessage::*;
326342

327-
// If the buffer is not empty, then the first frame must be a HEADERS
328-
// frame or the user violated the contract.
329-
match stream.pending_recv.pop_front(&mut self.buffer) {
330-
Some(Event::Headers(Client(response))) => Poll::Ready(Ok(response)),
331-
Some(_) => panic!("poll_response called after response returned"),
332-
None => {
333-
if !stream.state.ensure_recv_open()? {
334-
proto_err!(stream: "poll_response: stream={:?} is not opened;", stream.id);
335-
return Poll::Ready(Err(Error::library_reset(
336-
stream.id,
337-
Reason::PROTOCOL_ERROR,
338-
)));
343+
// Skip over any interim informational headers to find the main response
344+
loop {
345+
match stream.pending_recv.pop_front(&mut self.buffer) {
346+
Some(Event::Headers(Client(response))) => return Poll::Ready(Ok(response)),
347+
Some(Event::InformationalHeaders(_)) => {
348+
// Skip interim informational headers - they should be consumed by poll_informational
349+
continue;
339350
}
351+
Some(_) => panic!("poll_response called after response returned"),
352+
None => {
353+
if !stream.state.ensure_recv_open()? {
354+
proto_err!(stream: "poll_response: stream={:?} is not opened;", stream.id);
355+
return Poll::Ready(Err(Error::library_reset(
356+
stream.id,
357+
Reason::PROTOCOL_ERROR,
358+
)));
359+
}
340360

341-
stream.recv_task = Some(cx.waker().clone());
342-
Poll::Pending
361+
stream.recv_task = Some(cx.waker().clone());
362+
return Poll::Pending;
363+
}
364+
}
365+
}
366+
}
367+
368+
/// Called by the client to get informational responses (1xx status codes)
369+
pub fn poll_informational(
370+
&mut self,
371+
cx: &Context,
372+
stream: &mut store::Ptr,
373+
) -> Poll<Option<Result<Response<()>, proto::Error>>> {
374+
use super::peer::PollMessage::*;
375+
376+
// Try to pop the front event and check if it's an informational response
377+
// If it's not, we put it back
378+
if let Some(event) = stream.pending_recv.pop_front(&mut self.buffer) {
379+
match event {
380+
Event::InformationalHeaders(Client(response)) => {
381+
// Found an informational response, return it
382+
return Poll::Ready(Some(Ok(response)));
383+
}
384+
other => {
385+
// Not an informational response, put it back at the front
386+
stream.pending_recv.push_front(&mut self.buffer, other);
387+
}
343388
}
344389
}
390+
391+
// No informational response available at the front
392+
if stream.state.ensure_recv_open()? {
393+
// Request to get notified once more frames arrive
394+
stream.recv_task = Some(cx.waker().clone());
395+
Poll::Pending
396+
} else {
397+
// No more frames will be received
398+
Poll::Ready(None)
399+
}
345400
}
346401

347402
/// Transition the stream based on receiving trailers

src/proto/streams/send.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,47 @@ impl Send {
167167
Ok(())
168168
}
169169

170+
/// Send interim informational headers (1xx responses) without changing stream state.
171+
/// This allows multiple interim informational responses to be sent before the final response.
172+
pub fn send_interim_informational_headers<B>(
173+
&mut self,
174+
frame: frame::Headers,
175+
buffer: &mut Buffer<Frame<B>>,
176+
stream: &mut store::Ptr,
177+
_counts: &mut Counts,
178+
task: &mut Option<Waker>,
179+
) -> Result<(), UserError> {
180+
tracing::trace!(
181+
"send_informational_headers_direct; frame={:?}; stream_id={:?}",
182+
frame,
183+
frame.stream_id()
184+
);
185+
186+
// Validate headers
187+
Self::check_headers(frame.fields())?;
188+
189+
// Ensure this is an informational response (1xx status code)
190+
if !frame.is_informational() {
191+
tracing::debug!(
192+
"send_informational_headers_direct called with non-informational frame"
193+
);
194+
return Err(UserError::UnexpectedFrameType);
195+
}
196+
197+
// Ensure the frame is not marked as end_stream for informational responses
198+
if frame.is_end_stream() {
199+
tracing::debug!("send_informational_headers_direct called with end_stream=true");
200+
return Err(UserError::UnexpectedFrameType);
201+
}
202+
203+
// Queue the frame for sending WITHOUT changing stream state
204+
// This is the key difference from send_headers - we don't call stream.state.send_open()
205+
self.prioritize
206+
.queue_frame(frame.into(), buffer, stream, task);
207+
208+
Ok(())
209+
}
210+
170211
/// Send an explicit RST_STREAM frame
171212
pub fn send_reset<B>(
172213
&mut self,

src/proto/streams/streams.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,42 @@ impl<B> StreamRef<B> {
11501150
}
11511151
}
11521152

1153+
pub fn send_informational_headers(&mut self, frame: frame::Headers) -> Result<(), UserError> {
1154+
let mut me = self.opaque.inner.lock().unwrap();
1155+
let me = &mut *me;
1156+
1157+
let stream = me.store.resolve(self.opaque.key);
1158+
let actions = &mut me.actions;
1159+
let mut send_buffer = self.send_buffer.inner.lock().unwrap();
1160+
let send_buffer = &mut *send_buffer;
1161+
1162+
me.counts.transition(stream, |counts, stream| {
1163+
// For informational responses (1xx), we need to send headers without
1164+
// changing the stream state. This allows multiple informational responses
1165+
// to be sent before the final response.
1166+
1167+
// Validate that this is actually an informational response
1168+
if !frame.is_informational() {
1169+
return Err(UserError::UnexpectedFrameType);
1170+
}
1171+
1172+
// Ensure the frame is not marked as end_stream for informational responses
1173+
if frame.is_end_stream() {
1174+
return Err(UserError::UnexpectedFrameType);
1175+
}
1176+
1177+
// Send the interim informational headers directly to the buffer without state changes
1178+
// This bypasses the normal send_headers flow that would transition the stream state
1179+
actions.send.send_interim_informational_headers(
1180+
frame,
1181+
send_buffer,
1182+
stream,
1183+
counts,
1184+
&mut actions.task,
1185+
)
1186+
})
1187+
}
1188+
11531189
pub fn send_response(
11541190
&mut self,
11551191
mut response: Response<()>,
@@ -1334,6 +1370,19 @@ impl OpaqueStreamRef {
13341370

13351371
me.actions.recv.poll_response(cx, &mut stream)
13361372
}
1373+
1374+
/// Called by a client to check for informational responses (1xx status codes)
1375+
pub fn poll_informational(
1376+
&mut self,
1377+
cx: &Context,
1378+
) -> Poll<Option<Result<Response<()>, proto::Error>>> {
1379+
let mut me = self.inner.lock().unwrap();
1380+
let me = &mut *me;
1381+
1382+
let mut stream = me.store.resolve(self.key);
1383+
1384+
me.actions.recv.poll_informational(cx, &mut stream)
1385+
}
13371386
/// Called by a client to check for a pushed request.
13381387
pub fn poll_pushed(
13391388
&mut self,

src/server.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,6 +1102,105 @@ impl Default for Builder {
11021102
// ===== impl SendResponse =====
11031103

11041104
impl<B: Buf> SendResponse<B> {
1105+
/// Send an interim informational response (1xx status codes)
1106+
///
1107+
/// This method can be called multiple times before calling `send_response()`
1108+
/// to send the final response. Only 1xx status codes are allowed.
1109+
///
1110+
/// Interim informational responses are used to provide early feedback to the client
1111+
/// before the final response is ready. Common examples include:
1112+
/// - 100 Continue: Indicates the client should continue with the request
1113+
/// - 103 Early Hints: Provides early hints about resources to preload
1114+
///
1115+
/// # Arguments
1116+
/// * `response` - HTTP response with 1xx status code and headers
1117+
///
1118+
/// # Returns
1119+
/// * `Ok(())` - Interim Informational response sent successfully
1120+
/// * `Err(Error)` - Failed to send (invalid status code, connection error, etc.)
1121+
///
1122+
/// # Examples
1123+
/// ```rust
1124+
/// use h2::server;
1125+
/// use http::{Response, StatusCode};
1126+
///
1127+
/// # async fn example(mut send_response: h2::server::SendResponse<bytes::Bytes>) -> Result<(), h2::Error> {
1128+
/// // Send 100 Continue before processing request body
1129+
/// let continue_response = Response::builder()
1130+
/// .status(StatusCode::CONTINUE)
1131+
/// .body(())
1132+
/// .unwrap();
1133+
/// send_response.send_informational(continue_response)?;
1134+
///
1135+
/// // Later send the final response
1136+
/// let final_response = Response::builder()
1137+
/// .status(StatusCode::OK)
1138+
/// .body(())
1139+
/// .unwrap();
1140+
/// let _stream = send_response.send_response(final_response, false)?;
1141+
/// # Ok(())
1142+
/// # }
1143+
/// ```
1144+
///
1145+
/// # Errors
1146+
/// This method will return an error if:
1147+
/// - The response status code is not in the 1xx range
1148+
/// - The final response has already been sent
1149+
/// - There is a connection-level error
1150+
pub fn send_informational(&mut self, response: Response<()>) -> Result<(), crate::Error> {
1151+
let stream_id = self.inner.stream_id();
1152+
let status = response.status();
1153+
1154+
tracing::debug!(
1155+
"h2::send_informational called with status: {} on stream: {:?}",
1156+
status,
1157+
stream_id
1158+
);
1159+
1160+
// Validate that this is an informational response (1xx status code)
1161+
if !response.status().is_informational() {
1162+
tracing::debug!(
1163+
"h2::invalid informational status code: {} on stream: {:?}",
1164+
status,
1165+
stream_id
1166+
);
1167+
// Return an error for invalid status codes
1168+
return Err(crate::Error::from(
1169+
UserError::InvalidInformationalStatusCode,
1170+
));
1171+
}
1172+
1173+
tracing::trace!(
1174+
"h2::converting informational response to HEADERS frame for stream: {:?}",
1175+
stream_id
1176+
);
1177+
1178+
// Convert the response to a HEADERS frame without END_STREAM flag
1179+
// Use the proper Peer::convert_send_message method for informational responses
1180+
let frame = Peer::convert_send_message(
1181+
stream_id, response, false, // NOT end_of_stream for informational responses
1182+
);
1183+
1184+
tracing::trace!(
1185+
"h2::sending interim informational headers frame for stream: {:?}",
1186+
stream_id
1187+
);
1188+
1189+
// Use the proper H2 streams API for sending interim informational headers
1190+
// This bypasses the normal response flow and allows multiple informational responses
1191+
let result = self
1192+
.inner
1193+
.send_informational_headers(frame)
1194+
.map_err(Into::into);
1195+
1196+
match &result {
1197+
Ok(()) => tracing::debug!("h2::Successfully sent informational headers"),
1198+
Err(e) => tracing::debug!("h2::Failed to send informational headers: {:?}", e),
1199+
}
1200+
1201+
result
1202+
}
1203+
11051204
/// Send a response to a client request.
11061205
///
11071206
/// On success, a [`SendStream`] instance is returned. This instance can be

0 commit comments

Comments
 (0)