Skip to content

Commit

Permalink
tokio-quiche: add support for sending additional headers
Browse files Browse the repository at this point in the history
Since quiche will reject multiple calls to `send_response*()` on the
same stream, we need to call `send_additional_headers()` after the
initial headers are sent.
  • Loading branch information
ghedo committed Feb 25, 2025
1 parent a6ba007 commit d610a8f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 7 deletions.
22 changes: 15 additions & 7 deletions tokio-quiche/src/http3/driver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,13 +634,21 @@ impl<H: DriverHooks> H3Driver<H> {
let stream_id = audit_stats.stream_id();

match frame {
OutboundFrame::Headers(headers) => conn.send_response_with_priority(
qconn,
stream_id,
headers,
&DEFAULT_PRIO,
false,
),
// Initial headers were already sent, send additional headers now.
#[cfg(not(feature = "gcongestion"))]
OutboundFrame::Headers(headers) if ctx.initial_headers_sent => conn
.send_additional_headers(qconn, stream_id, headers, false, false),

// Send initial headers.
OutboundFrame::Headers(headers) => conn
.send_response_with_priority(
qconn,
stream_id,
headers,
&DEFAULT_PRIO,
false,
)
.inspect(|_| ctx.initial_headers_sent = true),

OutboundFrame::Body(body, fin) => {
let len = body.as_ref().len();
Expand Down
4 changes: 4 additions & 0 deletions tokio-quiche/src/http3/driver/streams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub(crate) struct StreamCtx {
/// This is used as temporary storage when waiting for `recv`.
pub(crate) queued_frame: Option<OutboundFrame>,
pub(crate) audit_stats: Arc<H3AuditStats>,
/// Indicates the stream sent initial headers.
pub(crate) initial_headers_sent: bool,
/// Indicates the stream received fin. No more data will be received.
pub(crate) fin_recv: bool,
/// Indicates the stream sent fin. No more data will be sent.
Expand All @@ -74,6 +76,8 @@ impl StreamCtx {
queued_frame: None,
audit_stats: Arc::new(H3AuditStats::new(stream_id)),

initial_headers_sent: false,

fin_recv: false,
fin_sent: false,

Expand Down
95 changes: 95 additions & 0 deletions tokio-quiche/tests/integration_tests/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (C) 2025, Cloudflare, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use crate::fixtures::*;

use futures::SinkExt;

use tokio_quiche::http3::driver::H3Event;
use tokio_quiche::http3::driver::IncomingH3Headers;
use tokio_quiche::http3::driver::OutboundFrame;
use tokio_quiche::http3::driver::ServerH3Event;
use tokio_quiche::quiche::h3::Header;

#[tokio::test]
async fn test_additional_headers() {
let hook = TestConnectionHook::new();

let url = start_server_with_settings(
QuicSettings::default(),
Http3Settings::default(),
hook,
move |mut h3_conn| async move {
let event_rx = h3_conn.h3_controller.event_receiver_mut();

while let Some(frame) = event_rx.recv().await {
let ServerH3Event::Core(frame) = frame;

match frame {
H3Event::IncomingHeaders(headers) => {
let IncomingH3Headers { mut send, .. } = headers;

// Send initial headers.
send.send(OutboundFrame::Headers(vec![Header::new(
b":status", b"103",
)]))
.await
.unwrap();

tokio::task::yield_now().await;

// Send additional headers.
send.send(OutboundFrame::Headers(vec![Header::new(
b":status", b"200",
)]))
.await
.unwrap();
},

H3Event::ConnectionShutdown(_) => break,

_ => (),
}
}
},
);

let summary = h3i_fixtures::request(&url, 1)
.await
.expect("request failed");

let mut headers = summary.stream_map.headers_on_stream(0).into_iter();

assert_eq!(
headers.next().expect("initial headers").status_code(),
Some(&Vec::from("103".as_bytes()))
);
assert_eq!(
headers.next().expect("additional headers").status_code(),
Some(&Vec::from("200".as_bytes()))
);
assert!(headers.next().is_none());
}
1 change: 1 addition & 0 deletions tokio-quiche/tests/integration_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use tokio_quiche::InitialQuicConnection;

pub mod async_callbacks;
pub mod connection_close;
pub mod headers;
pub mod timeouts;

#[tokio::test]
Expand Down

0 comments on commit d610a8f

Please sign in to comment.