@@ -50,6 +50,13 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpH
5050
5151 private TaskCompletionSource ? _appCompleted ;
5252
53+ private StreamCompletionFlags _completionState ;
54+ private readonly object _completionLock = new object ( ) ;
55+
56+ public bool EndStreamReceived => ( _completionState & StreamCompletionFlags . EndStreamReceived ) == StreamCompletionFlags . EndStreamReceived ;
57+ private bool IsAborted => ( _completionState & StreamCompletionFlags . Aborted ) == StreamCompletionFlags . Aborted ;
58+ internal bool RstStreamReceived => ( _completionState & StreamCompletionFlags . RstStreamReceived ) == StreamCompletionFlags . RstStreamReceived ;
59+
5360 public Pipe RequestBodyPipe { get ; }
5461
5562 public Http3Stream ( Http3StreamContext context )
@@ -103,8 +110,12 @@ public Http3Stream(Http3StreamContext context)
103110
104111 public void Abort ( ConnectionAbortedException abortReason , Http3ErrorCode errorCode )
105112 {
106- // TODO - Should there be a check here to track abort state to avoid
107- // running twice for a request?
113+ var ( oldState , newState ) = ApplyCompletionFlag ( StreamCompletionFlags . Aborted ) ;
114+
115+ if ( oldState == newState )
116+ {
117+ return ;
118+ }
108119
109120 Log . Http3StreamAbort ( TraceIdentifier , errorCode , abortReason ) ;
110121
@@ -316,9 +327,36 @@ private static bool IsConnectionSpecificHeaderField(ReadOnlySpan<byte> name, Rea
316327 }
317328
318329 protected override void OnRequestProcessingEnded ( )
330+ {
331+ CompleteStream ( errored : false ) ;
332+ }
333+
334+ private void CompleteStream ( bool errored )
319335 {
320336 Debug . Assert ( _appCompleted != null ) ;
321337 _appCompleted . SetResult ( ) ;
338+
339+ if ( ! EndStreamReceived )
340+ {
341+ if ( ! errored )
342+ {
343+ Log . RequestBodyNotEntirelyRead ( ConnectionIdFeature , TraceIdentifier ) ;
344+ }
345+
346+ var ( oldState , newState ) = ApplyCompletionFlag ( StreamCompletionFlags . Aborted ) ;
347+ if ( oldState != newState )
348+ {
349+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1-15
350+ // When the server does not need to receive the remainder of the request, it MAY abort reading
351+ // the request stream, send a complete response, and cleanly close the sending part of the stream.
352+ // The error code H3_NO_ERROR SHOULD be used when requesting that the client stop sending on the
353+ // request stream.
354+
355+ // TODO(JamesNK): Abort the read half of the stream with H3_NO_ERROR
356+
357+ RequestBodyPipe . Writer . Complete ( ) ;
358+ }
359+ }
322360 }
323361
324362 private bool TryClose ( )
@@ -421,6 +459,8 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
421459
422460 private ValueTask OnEndStreamReceived ( )
423461 {
462+ ApplyCompletionFlag ( StreamCompletionFlags . EndStreamReceived ) ;
463+
424464 if ( _requestHeaderParsingState == RequestHeaderParsingState . Ready )
425465 {
426466 // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1-14
@@ -542,7 +582,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence<byte> payload)
542582 // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1
543583 if ( _requestHeaderParsingState == RequestHeaderParsingState . Trailers )
544584 {
545- var message = CoreStrings . FormatHttp3StreamErrorFrameReceivedAfterTrailers ( CoreStrings . FormatHttp3StreamErrorFrameReceivedAfterTrailers ( Http3Formatting . ToFormattedType ( Http3FrameType . Data ) ) ) ;
585+ var message = CoreStrings . FormatHttp3StreamErrorFrameReceivedAfterTrailers ( Http3Formatting . ToFormattedType ( Http3FrameType . Data ) ) ;
546586 throw new Http3ConnectionErrorException ( message , Http3ErrorCode . UnexpectedFrame ) ;
547587 }
548588
@@ -804,6 +844,17 @@ private Pipe CreateRequestBodyPipe(uint windowSize)
804844 minimumSegmentSize : _context . MemoryPool . GetMinimumSegmentSize ( )
805845 ) ) ;
806846
847+ private ( StreamCompletionFlags OldState , StreamCompletionFlags NewState ) ApplyCompletionFlag ( StreamCompletionFlags completionState )
848+ {
849+ lock ( _completionLock )
850+ {
851+ var oldCompletionState = _completionState ;
852+ _completionState |= completionState ;
853+
854+ return ( oldCompletionState , _completionState ) ;
855+ }
856+ }
857+
807858 /// <summary>
808859 /// Used to kick off the request processing loop by derived classes.
809860 /// </summary>
@@ -829,6 +880,15 @@ private enum PseudoHeaderFields
829880 Unknown = 0x40000000
830881 }
831882
883+ [ Flags ]
884+ private enum StreamCompletionFlags
885+ {
886+ None = 0 ,
887+ RstStreamReceived = 1 ,
888+ EndStreamReceived = 2 ,
889+ Aborted = 4 ,
890+ }
891+
832892 private static class GracefulCloseInitiator
833893 {
834894 public const int None = 0 ;
0 commit comments