Skip to content

Commit 19efbcf

Browse files
authored
HTTP/3: Improve QUIC stream and HTTP/3 conn completion errors (#35210)
1 parent 715d519 commit 19efbcf

File tree

7 files changed

+47
-27
lines changed

7 files changed

+47
-27
lines changed

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,6 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
357357
}
358358
finally
359359
{
360-
// TODO should this message say the connection is shutting down or closing rather than faulted?
361-
// Faulted has a negative meaning and we can get here by the host stopping or ConnectionLifeTimeNotification.RequestClose.
362-
var connectionError = error as ConnectionAbortedException
363-
?? new ConnectionAbortedException(CoreStrings.Http3ConnectionFaulted, error!);
364-
365360
try
366361
{
367362
// Don't try to send GOAWAY if the client has already closed the connection.
@@ -378,19 +373,21 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
378373
{
379374
foreach (var stream in _streams.Values)
380375
{
381-
stream.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
376+
stream.Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);
382377
}
383378
}
384379

385-
// Complete outbound control stream.
386380
if (outboundControlStream != null)
387381
{
388-
await outboundControlStream.CompleteAsync();
382+
// Don't gracefully close the outbound control stream. If the peer detects
383+
// the control stream closes it will close with a procotol error.
384+
// Instead, allow control stream to be automatically aborted when the
385+
// connection is aborted.
389386
await outboundControlStreamTask;
390387
}
391388

392389
// Complete
393-
Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
390+
Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error);
394391

395392
// Wait for active requests to complete.
396393
while (_activeRequestCount > 0)
@@ -403,7 +400,7 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
403400
}
404401
catch
405402
{
406-
Abort(connectionError, Http3ErrorCode.InternalError);
403+
Abort(CreateConnectionAbortError(error, clientAbort), Http3ErrorCode.InternalError);
407404
throw;
408405
}
409406
finally
@@ -418,6 +415,21 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
418415
}
419416
}
420417

418+
private ConnectionAbortedException CreateConnectionAbortError(Exception? error, bool clientAbort)
419+
{
420+
if (error is ConnectionAbortedException abortedException)
421+
{
422+
return abortedException;
423+
}
424+
425+
if (clientAbort)
426+
{
427+
return new ConnectionAbortedException("The client closed the HTTP/3 connection.", error!);
428+
}
429+
430+
return new ConnectionAbortedException(CoreStrings.Http3ConnectionFaulted, error!);
431+
}
432+
421433
private Http3StreamContext CreateHttpStreamContext(ConnectionContext streamContext)
422434
{
423435
var httpConnectionContext = new Http3StreamContext(
@@ -657,8 +669,7 @@ public void OnInputOrOutputCompleted()
657669
{
658670
TryStopAcceptingStreams();
659671

660-
Debug.Assert(Enum.IsDefined((Http3ErrorCode)_errorCodeFeature.Error), "Known HTTP/3 error code should be set.");
661-
672+
// Abort the connection using the error code the client used. For a graceful close, this should be H3_NO_ERROR.
662673
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), (Http3ErrorCode)_errorCodeFeature.Error);
663674
}
664675

src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,6 @@ private void ProcessSetting(long id, long value)
344344
}
345345
}
346346

347-
internal ValueTask CompleteAsync()
348-
{
349-
return _frameWriter.CompleteAsync();
350-
}
351-
352347
private ValueTask ProcessGoAwayFrameAsync()
353348
{
354349
EnsureSettingsFrame(Http3FrameType.GoAway);

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -463,12 +463,12 @@ public async Task ProcessRequestAsync<TContext>(IHttpApplication<TContext> appli
463463

464464
_context.StreamLifetimeHandler.OnStreamConnectionError(ex);
465465
}
466+
catch (ConnectionAbortedException ex)
467+
{
468+
error = ex;
469+
}
466470
catch (ConnectionResetException ex)
467471
{
468-
// TODO: This is temporary. Don't want to tie HTTP/3 layer to one transport.
469-
// This is here to check what other exceptions can cause ConnectionResetException.
470-
Debug.Assert(ex.InnerException is QuicStreamAbortedException);
471-
472472
error = ex;
473473
Abort(new ConnectionAbortedException(ex.Message, ex), (Http3ErrorCode)_errorCodeFeature.Error);
474474
}

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ private async Task DoSend()
353353
}
354354
finally
355355
{
356-
ShutdownWrite(shutdownReason);
356+
ShutdownWrite(_shutdownWriteReason ?? _shutdownReason ?? shutdownReason);
357357

358358
// Complete the output after disposing the stream
359359
Output.Complete(unexpectedError ?? shutdownReason);
@@ -373,6 +373,7 @@ public override void Abort(ConnectionAbortedException abortReason)
373373
}
374374

375375
_serverAborted = true;
376+
_shutdownReason = abortReason;
376377

377378
_log.StreamAbort(this, Error, abortReason.Message);
378379

src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public async Task BidirectionalStream_ServerReadsDataAndCompletes_GracefullyClos
4646
var quicConnectionContext = Assert.IsType<QuicConnectionContext>(serverConnection);
4747

4848
Assert.Equal(1, quicConnectionContext.StreamPool.Count);
49+
50+
Assert.Contains(TestSink.Writes, m => m.Message.Contains(@"shutting down writes because: ""The QUIC transport's send loop completed gracefully.""."));
4951
}
5052

5153
[ConditionalTheory]
@@ -292,6 +294,8 @@ public async Task ServerToClientUnidirectionalStream_ServerWritesDataAndComplete
292294

293295
// Both send and receive loops have exited.
294296
await quicStreamContext._processingTask.DefaultTimeout();
297+
298+
Assert.Contains(TestSink.Writes, m => m.Message.Contains(@"shutting down writes because: ""The QUIC transport's send loop completed gracefully.""."));
295299
}
296300

297301
[ConditionalFact]
@@ -333,6 +337,8 @@ public async Task ServerToClientUnidirectionalStream_ServerAborts_ClientGetsAbor
333337

334338
// Both send and receive loops have exited.
335339
await quicStreamContext._processingTask.DefaultTimeout();
340+
341+
Assert.Contains(TestSink.Writes, m => m.Message.Contains(@"shutting down writes because: ""Test message""."));
336342
}
337343

338344
[ConditionalFact]

src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,6 @@ internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAway
150150
#else
151151
await _connectionTask;
152152
#endif
153-
154-
// Verify server-to-client control stream has completed.
155-
await _inboundControlStream.ReceiveEndAsync();
156153
}
157154

158155
internal async Task WaitForGoAwayAsync(bool ignoreNonGoAwayFrames, long? expectedLastStreamId)

src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -880,8 +880,18 @@ await WaitForLogAsync(logs =>
880880
await WaitForLogAsync(logs =>
881881
{
882882
const int applicationAbortedConnectionId = 6;
883-
return logs.Any(w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" &&
884-
w.EventId == applicationAbortedConnectionId);
883+
var connectionAbortLog = logs.FirstOrDefault(
884+
w => w.LoggerName == "Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" &&
885+
w.EventId == applicationAbortedConnectionId);
886+
if (connectionAbortLog == null)
887+
{
888+
return false;
889+
}
890+
891+
// This message says the client closed the connection because the server
892+
// sends a GOAWAY and the client then closes the connection once all requests are finished.
893+
Assert.Contains("The client closed the connection.", connectionAbortLog.Message);
894+
return true;
885895
}, "Wait for connection abort.");
886896

887897
Logger.LogInformation("Sending request after connection abort.");

0 commit comments

Comments
 (0)