Skip to content

Commit 82ed1bd

Browse files
committed
Select endpoint STOP_SENDING code after local FIN
Quiche acceptance of a FIN-bearing HTTP/3 write is tracked separately from open and aborted send directions. DriverHooks::STOP_SENDING_CODE_AFTER_LOCAL_FIN selects the code when the driver detects a dropped inbound-body receiver, without implying peer delivery or acknowledgement. Servers use H3_NO_ERROR after submitted responses. Clients, queued or blocked FINs, open responses, and aborted responses keep H3_REQUEST_CANCELLED. Focused coverage pins body and trailers FIN, queued and blocked FIN timing, receiver-drop detection timing, local reset, and peer STOP transitions.
1 parent c06ccd6 commit 82ed1bd

6 files changed

Lines changed: 580 additions & 57 deletions

File tree

‎tokio-quiche/src/http3/driver/client.rs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,8 @@ impl ClientHooks {
218218
);
219219

220220
if body_finished {
221-
// `send_request()` already sent FIN for bodyless requests, so
222-
// mark the send side complete and drop the outbound-frame receiver
223-
// since no body will be sent.
224-
H3Driver::<ClientHooks>::on_fin_sent(&mut stream_ctx)?;
221+
// `send_request()` accepted FIN for this bodyless request.
222+
stream_ctx.handle_sent_fin();
225223
}
226224

227225
if let Some(quarter_stream_id) =
@@ -302,6 +300,9 @@ impl DriverHooks for ClientHooks {
302300
type Command = ClientH3Command;
303301
type Event = ClientH3Event;
304302

303+
const STOP_SENDING_CODE_AFTER_LOCAL_FIN: h3::WireErrorCode =
304+
h3::WireErrorCode::RequestCancelled;
305+
305306
fn new(_settings: &Http3Settings) -> Self {
306307
Self {
307308
pending_requests: BTreeMap::new(),

‎tokio-quiche/src/http3/driver/hooks.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ pub trait DriverHooks: Sized + Send + 'static {
5757
/// The concrete type is expected to wrap [`H3Command`].
5858
type Command: From<H3Command> + Send;
5959

60+
/// Error code used when the driver detects that the application dropped
61+
/// its inbound-body receiver before the peer's send direction ended, after
62+
/// quiche accepted a FIN-bearing HTTP/3 write on the local send direction.
63+
const STOP_SENDING_CODE_AFTER_LOCAL_FIN: h3::WireErrorCode;
64+
6065
/// Initializes the storage for these hooks.
6166
fn new(settings: &Http3Settings) -> Self;
6267

‎tokio-quiche/src/http3/driver/mod.rs‎

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,11 @@ impl<H: DriverHooks> H3Driver<H> {
535535
Ok(permit) => permit,
536536
Err(TrySendError::Closed(())) => {
537537
// The channel has closed before we delivered a fin or reset
538-
// to the application.
538+
// to the application. Choose the code from the send state
539+
// when the driver detects the closure: quiche may have
540+
// accepted local FIN after the application dropped this
541+
// receiver. A FIN still queued or blocked leaves the send
542+
// side open.
539543
// Release the channel borrow before updating the context.
540544
drop(try_reserve_result);
541545
match ctx.recv_state {
@@ -544,8 +548,13 @@ impl<H: DriverHooks> H3Driver<H> {
544548
// An associated DATAGRAM flow can outlive its body
545549
// channel.
546550
{
547-
let err =
548-
h3::WireErrorCode::RequestCancelled as u64;
551+
let wire_err_code = match ctx.send_state() {
552+
SendState::Finished =>
553+
H::STOP_SENDING_CODE_AFTER_LOCAL_FIN,
554+
SendState::Open | SendState::Aborted =>
555+
h3::WireErrorCode::RequestCancelled,
556+
};
557+
let err = wire_err_code as u64;
549558
let _ = qconn.stream_shutdown(
550559
stream_id,
551560
quiche::Shutdown::Read,
@@ -866,7 +875,9 @@ impl<H: DriverHooks> H3Driver<H> {
866875
let len = body.len();
867876

868877
if *fin {
869-
// Drop the receiver on FIN to prevent more frames.
878+
// Drop the receiver while FIN is pending to prevent more
879+
// frames. SendState remains Open until quiche accepts the
880+
// entire FIN-bearing write.
870881
// We cannot use `mpsc::Receiver::close()` because of a
871882
// Tokio inconsistency when reading a closed channel.
872883
// See https://github.com/tokio-rs/tokio/issues/7631.
@@ -889,7 +900,7 @@ impl<H: DriverHooks> H3Driver<H> {
889900
Err(h3::Error::StreamBlocked)
890901
} else {
891902
if *fin {
892-
Self::on_fin_sent(ctx)?;
903+
Self::on_fin_accepted(ctx)?;
893904
}
894905

895906
Ok(())
@@ -909,7 +920,7 @@ impl<H: DriverHooks> H3Driver<H> {
909920
);
910921

911922
if res.is_ok() {
912-
Self::on_fin_sent(ctx)?;
923+
Self::on_fin_accepted(ctx)?;
913924
}
914925

915926
return res;
@@ -935,22 +946,8 @@ impl<H: DriverHooks> H3Driver<H> {
935946
Ok(())
936947
}
937948

938-
fn on_fin_sent(ctx: &mut StreamCtx) -> h3::Result<()> {
939-
match ctx.send_state {
940-
SendState::Open => {},
941-
SendState::Closed => {
942-
debug_assert!(
943-
false,
944-
"FIN completion requires an open send direction"
945-
);
946-
return Err(h3::Error::InternalError);
947-
},
948-
}
949-
ctx.cancel_recv_wait();
950-
ctx.recv = None;
951-
ctx.send_state = SendState::Closed;
952-
ctx.audit_stats
953-
.set_sent_stream_fin(StreamClosureKind::Explicit);
949+
fn on_fin_accepted(ctx: &mut StreamCtx) -> h3::Result<()> {
950+
ctx.handle_sent_fin();
954951
if ctx.fully_closed() {
955952
// Return a TransportError to trigger stream cleanup
956953
// instead of h3::Error::Done.
@@ -1269,14 +1266,15 @@ impl<H: DriverHooks> H3Driver<H> {
12691266
match recv.try_recv() {
12701267
Ok(frame) => ctx.queued_frame = Some(frame),
12711268
Err(TryRecvError::Disconnected) => {
1272-
match ctx.send_state {
1269+
match ctx.send_state() {
12731270
SendState::Open => {
12741271
if ctx.associated_dgram_flow_id.is_none()
12751272
// An associated DATAGRAM flow can outlive its body
12761273
// channel.
12771274
{
1278-
// No FIN was written before channel closure.
1279-
// Reset because no more data will follow.
1275+
// No FIN-bearing write was accepted before
1276+
// channel closure. Reset because no more data
1277+
// will follow.
12801278
let err =
12811279
h3::WireErrorCode::RequestCancelled as u64;
12821280
let _ = qconn.stream_shutdown(
@@ -1290,7 +1288,7 @@ impl<H: DriverHooks> H3Driver<H> {
12901288
}
12911289
}
12921290
},
1293-
SendState::Closed => {},
1291+
SendState::Finished | SendState::Aborted => {},
12941292
}
12951293
break;
12961294
},

‎tokio-quiche/src/http3/driver/server.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use std::ops::Deref;
2828
use std::sync::Arc;
2929

30+
use quiche::h3;
3031
use tokio::sync::mpsc;
3132

3233
use super::datagram;
@@ -267,6 +268,9 @@ impl DriverHooks for ServerHooks {
267268
type Command = ServerH3Command;
268269
type Event = ServerH3Event;
269270

271+
const STOP_SENDING_CODE_AFTER_LOCAL_FIN: h3::WireErrorCode =
272+
h3::WireErrorCode::NoError;
273+
270274
fn new(settings: &Http3Settings) -> Self {
271275
Self {
272276
settings_enforcer: settings.into(),

‎tokio-quiche/src/http3/driver/streams.rs‎

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use std::task::Wake;
3434
use std::task::Waker;
3535
use std::time::Instant;
3636

37+
use datagram_socket::StreamClosureKind;
3738
use quiche::h3;
3839
use tokio::sync::mpsc;
3940
use tokio_util::sync::PollSender;
@@ -156,13 +157,16 @@ impl RecvState {
156157
}
157158
}
158159

159-
/// The transport write direction is closed by FIN, reset, or peer STOP.
160+
/// Local HTTP/3 write-side lifecycle.
160161
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161162
pub(super) enum SendState {
162-
/// Transport writes have not yet finished or been canceled.
163+
/// No FIN-bearing write was accepted and the write side was not aborted.
163164
Open,
164-
/// Transport writes ended through FIN, local reset, or peer STOP_SENDING.
165-
Closed,
165+
/// Quiche accepted a FIN-bearing HTTP/3 write.
166+
Finished,
167+
/// The write side ended via reset or peer STOP_SENDING without accepted
168+
/// FIN.
169+
Aborted,
166170
}
167171

168172
/// Receives coalesced stream notifications and rearms their wake state.
@@ -250,7 +254,8 @@ pub(crate) struct StreamCtx {
250254
/// Receive-side lifecycle and the sender for local application delivery.
251255
pub(super) recv_state: RecvState,
252256
/// Transport write-side lifecycle.
253-
pub(super) send_state: SendState,
257+
/// Tracks local FIN acceptance or write-side abortion.
258+
send_state: SendState,
254259
/// The flow ID for proxying datagrams over this stream. If `None`,
255260
/// the stream has no associated DATAGRAM flow.
256261
pub(crate) associated_dgram_flow_id: Option<u64>,
@@ -297,12 +302,16 @@ impl StreamCtx {
297302
/// Cleanup requires closed transport writes and closed app delivery.
298303
pub(super) fn fully_closed(&self) -> bool {
299304
match (&self.recv_state, self.send_state) {
300-
(RecvState::Closed, SendState::Closed) => true,
305+
(RecvState::Closed, SendState::Finished | SendState::Aborted) => true,
301306
(RecvState::Open(_) | RecvState::FinPending(_), _) |
302307
(RecvState::Closed, SendState::Open) => false,
303308
}
304309
}
305310

311+
pub(super) fn send_state(&self) -> SendState {
312+
self.send_state
313+
}
314+
306315
/// Schedules the initial poll to register for upstream channel capacity.
307316
/// Subsequent readiness uses the same coalescing ready queue.
308317
pub(crate) fn wait_for_send(&mut self) {
@@ -327,6 +336,7 @@ impl StreamCtx {
327336
self.waiting_for_recv
328337
}
329338

339+
#[cfg(test)]
330340
pub(super) fn cancel_recv_wait(&mut self) {
331341
self.waiting_for_recv = false;
332342
}
@@ -365,22 +375,23 @@ impl StreamCtx {
365375
Ok((recv_ready, send_ready))
366376
}
367377

368-
/// Handle the case when we received a STOP_SENDING frame. Note, that
369-
/// we'll only learn about a STOP_SENDING frame from the write path.
370-
/// Also note, that quiche will automatically send a RESET_STREAM frame
371-
/// in response when it receives a STOP_SENDING (as recommended by the
372-
/// RFC)
378+
/// Records a peer STOP_SENDING observed from the write path.
379+
///
380+
/// Quiche automatically sends RESET_STREAM in response, as recommended by
381+
/// the RFC.
373382
pub(crate) fn handle_recvd_stop_sending(&mut self, wire_err_code: u64) {
374383
// Update stats even if the stream is already closing.
375384
self.audit_stats
376385
.set_recvd_stop_sending_error_code(wire_err_code as i64);
377386

378387
match self.send_state {
379-
SendState::Closed => {
380-
// STOP may arrive after FIN or reset has closed the write side.
388+
SendState::Finished | SendState::Aborted => {
389+
// A terminal send state stays terminal. In particular, a later
390+
// peer STOP_SENDING does not undo quiche's prior acceptance of
391+
// local FIN or imply that the peer received or acknowledged it.
381392
return;
382393
},
383-
SendState::Open => self.send_state = SendState::Closed,
394+
SendState::Open => self.send_state = SendState::Aborted,
384395
}
385396

386397
// We received a STOP_SENDING frame. This indicates that the
@@ -423,7 +434,7 @@ impl StreamCtx {
423434
) -> H3ConnectionResult<()> {
424435
match self.send_state {
425436
SendState::Open => {},
426-
SendState::Closed => {
437+
SendState::Finished | SendState::Aborted => {
427438
debug_assert!(
428439
false,
429440
"A local reset requires an open send direction"
@@ -433,11 +444,25 @@ impl StreamCtx {
433444
}
434445
self.audit_stats
435446
.set_sent_reset_stream_error_code(wire_err_code as i64);
436-
self.send_state = SendState::Closed;
447+
self.send_state = SendState::Aborted;
437448
self.waiting_for_recv = false;
438449
Ok(())
439450
}
440451

452+
pub(crate) fn handle_sent_fin(&mut self) {
453+
// This records local submission to quiche, not peer receipt or ACK.
454+
debug_assert_eq!(
455+
self.send_state,
456+
SendState::Open,
457+
"accepted FIN requires an open send direction"
458+
);
459+
self.recv = None;
460+
self.waiting_for_recv = false;
461+
self.send_state = SendState::Finished;
462+
self.audit_stats
463+
.set_sent_stream_fin(StreamClosureKind::Explicit);
464+
}
465+
441466
pub(crate) fn handle_sent_stop_sending(
442467
&mut self, wire_err_code: u64,
443468
) -> H3ConnectionResult<()> {
@@ -812,7 +837,7 @@ mod tests {
812837
let (mut ctx, _outbound, _inbound) = StreamCtx::new(4, 1, ready_tx);
813838
cancel(&mut ctx, 42).unwrap();
814839
assert_eq!(matches!(ctx.recv_state, RecvState::Closed), closes_receive);
815-
assert_eq!(ctx.send_state == SendState::Closed, !closes_receive);
840+
assert_eq!(ctx.send_state == SendState::Aborted, !closes_receive);
816841

817842
// Only the second call is allowed to reject the closed direction.
818843
let result = catch_unwind(AssertUnwindSafe(|| cancel(&mut ctx, 43)));

0 commit comments

Comments
 (0)