@@ -34,6 +34,7 @@ use std::task::Wake;
3434use std:: task:: Waker ;
3535use std:: time:: Instant ;
3636
37+ use datagram_socket:: StreamClosureKind ;
3738use quiche:: h3;
3839use tokio:: sync:: mpsc;
3940use 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 ) ]
161162pub ( 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