@@ -133,6 +133,11 @@ public void InitializeWithExistingContext(IDuplexPipe transport)
133133 }
134134
135135 public void Abort ( ConnectionAbortedException abortReason , Http3ErrorCode errorCode )
136+ {
137+ AbortCore ( abortReason , errorCode ) ;
138+ }
139+
140+ private void AbortCore ( Exception exception , Http3ErrorCode errorCode )
136141 {
137142 lock ( _completionLock )
138143 {
@@ -148,26 +153,27 @@ public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCo
148153 return ;
149154 }
150155
151- Log . Http3StreamAbort ( TraceIdentifier , errorCode , abortReason ) ;
156+ if ( ! ( exception is ConnectionAbortedException abortReason ) )
157+ {
158+ abortReason = new ConnectionAbortedException ( exception . Message , exception ) ;
159+ }
152160
153- _errorCodeFeature . Error = ( long ) errorCode ;
154- _frameWriter . Abort ( abortReason ) ;
161+ Log . Http3StreamAbort ( TraceIdentifier , errorCode , abortReason ) ;
155162
156- AbortCore ( abortReason ) ;
157- }
158- }
163+ // Call _http3Output.Stop() prior to poisoning the request body stream or pipe to
164+ // ensure that an app that completes early due to the abort doesn't result in header frames being sent.
165+ _http3Output . Stop ( ) ;
159166
160- private void AbortCore ( Exception exception )
161- {
162- // Call _http3Output.Stop() prior to poisoning the request body stream or pipe to
163- // ensure that an app that completes early due to the abort doesn't result in header frames being sent.
164- _http3Output . Stop ( ) ;
167+ CancelRequestAbortedToken ( ) ;
165168
166- CancelRequestAbortedToken ( ) ;
169+ // Unblock the request body.
170+ PoisonBody ( exception ) ;
171+ RequestBodyPipe . Writer . Complete ( exception ) ;
167172
168- // Unblock the request body.
169- PoisonBody ( exception ) ;
170- RequestBodyPipe . Writer . Complete ( exception ) ;
173+ // Abort framewriter and underlying transport after stopping output.
174+ _errorCodeFeature . Error = ( long ) errorCode ;
175+ _frameWriter . Abort ( abortReason ) ;
176+ }
171177 }
172178
173179 protected override void OnErrorAfterResponseStarted ( )
@@ -470,7 +476,8 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
470476 catch ( ConnectionResetException ex )
471477 {
472478 error = ex ;
473- AbortCore ( new IOException ( CoreStrings . HttpStreamResetByClient , ex ) ) ;
479+ var resolvedErrorCode = _errorCodeFeature . Error >= 0 ? _errorCodeFeature . Error : 0 ;
480+ AbortCore ( new IOException ( CoreStrings . HttpStreamResetByClient , ex ) , ( Http3ErrorCode ) resolvedErrorCode ) ;
474481 }
475482 catch ( Exception ex )
476483 {
@@ -484,10 +491,44 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
484491
485492 await Input . CompleteAsync ( ) ;
486493
487- // Make sure application func is completed before completing writer.
488- if ( _appCompleted != null )
494+ var appCompleted = _appCompleted ? . Task ?? Task . CompletedTask ;
495+ if ( ! appCompleted . IsCompletedSuccessfully )
489496 {
490- await _appCompleted . Task ;
497+ // At this point in the stream's read-side is complete. However, with HTTP/3
498+ // the write-side of the stream can still be aborted by the client on request
499+ // aborted.
500+ //
501+ // To get notification of request aborted we register to connection closed
502+ // token. It will notify this type that the client has aborted the request
503+ // and Kestrel will complete pipes and cancel the RequestAborted token.
504+ //
505+ // Only subscribe to this event after the stream's read-side is complete to
506+ // avoid interactions between reading that is in-progress and an abort.
507+ // This means while reading, read-side abort will handle getting abort notifications.
508+ //
509+ // We don't need to hang on to the CancellationTokenRegistration from register.
510+ // The CTS is cleaned up in StreamContext.DisposeAsync.
511+ //
512+ // TODO: Consider a better way to provide this notification. For perf we want to
513+ // make the ConnectionClosed CTS pay-for-play, and change this event to use
514+ // something that is more lightweight than a CTS.
515+ _context . StreamContext . ConnectionClosed . Register ( static s =>
516+ {
517+ var stream = ( Http3Stream ) s ! ;
518+
519+ if ( ! stream . IsCompleted )
520+ {
521+ // An error code value other than -1 indicates a value was set and the request didn't gracefully complete.
522+ var errorCode = stream . _errorCodeFeature . Error ;
523+ if ( errorCode >= 0 )
524+ {
525+ stream . AbortCore ( new IOException ( CoreStrings . HttpStreamResetByClient ) , ( Http3ErrorCode ) errorCode ) ;
526+ }
527+ }
528+ } , this ) ;
529+
530+ // Make sure application func is completed before completing writer.
531+ await appCompleted ;
491532 }
492533
493534 try
0 commit comments