Skip to content

Commit 83cc8be

Browse files
authored
HTTP/3: Complete outbound control stream with connection (#33956)
1 parent b9d7c32 commit 83cc8be

File tree

4 files changed

+58
-25
lines changed

4 files changed

+58
-25
lines changed

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

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,21 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
224224
// the maximum capacity of the dynamic table to zero.
225225

226226
// Don't create Encoder and Decoder as they aren't used now.
227-
Exception? error = null;
228227

229-
// Don't delay setting up the connection on the control stream being ready.
230-
// Task is awaited when connection finishes.
231-
var controlStreamTask = CreateControlStreamAsync(application);
228+
Exception? error = null;
229+
ValueTask outboundControlStreamTask = default;
232230

233231
try
234232
{
233+
var outboundControlStream = await CreateNewUnidirectionalStreamAsync(application);
234+
lock (_sync)
235+
{
236+
OutboundControlStream = outboundControlStream;
237+
}
238+
239+
// Don't delay on waiting to send outbound control stream settings.
240+
outboundControlStreamTask = ProcessOutboundControlStreamAsync(outboundControlStream);
241+
235242
while (_isClosed == 0)
236243
{
237244
// Don't pass a cancellation token to AcceptAsync.
@@ -341,23 +348,26 @@ public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> appl
341348
stream.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
342349
}
343350

351+
lock (_sync)
352+
{
353+
OutboundControlStream?.Abort(connectionError, (Http3ErrorCode)_errorCodeFeature.Error);
354+
}
355+
344356
while (_activeRequestCount > 0)
345357
{
346358
await _streamCompletionAwaitable;
347359
}
348360

361+
await outboundControlStreamTask;
362+
349363
_context.TimeoutControl.CancelTimeout();
350364
_context.TimeoutControl.StartDrainTimeout(Limits.MinResponseDataRate, Limits.MaxResponseBufferSize);
351365
}
352366
catch
353367
{
354-
Abort(connectionError, Http3ErrorCode.NoError);
368+
Abort(connectionError, Http3ErrorCode.InternalError);
355369
throw;
356370
}
357-
358-
// Ensure control stream creation task finished. At this point the connection, including the control
359-
// stream should be closed/aborted. Error handling inside method ensures await won't throw.
360-
await controlStreamTask;
361371
}
362372
}
363373

@@ -400,18 +410,12 @@ private void UpdateConnectionState()
400410
}
401411
}
402412

403-
private async ValueTask CreateControlStreamAsync<TContext>(IHttpApplication<TContext> application) where TContext : notnull
413+
private async ValueTask ProcessOutboundControlStreamAsync(Http3ControlStream controlStream)
404414
{
405415
try
406416
{
407-
var stream = await CreateNewUnidirectionalStreamAsync(application);
408-
lock (_sync)
409-
{
410-
OutboundControlStream = stream;
411-
}
412-
413-
await stream.SendStreamIdAsync(id: 0);
414-
await stream.SendSettingsFrameAsync();
417+
await controlStream.SendStreamIdAsync(id: 0);
418+
await controlStream.SendSettingsFrameAsync();
415419
}
416420
catch (Exception ex)
417421
{
@@ -449,15 +453,27 @@ private async ValueTask<Http3ControlStream> CreateNewUnidirectionalStreamAsync<T
449453
return new Http3ControlStream<TContext>(application, httpConnectionContext);
450454
}
451455

452-
private ValueTask<FlushResult> SendGoAway(long id)
456+
private async ValueTask<FlushResult> SendGoAway(long id)
453457
{
458+
Http3ControlStream? stream;
454459
lock (_sync)
455460
{
456-
if (OutboundControlStream != null)
461+
stream = OutboundControlStream;
462+
}
463+
464+
if (stream != null)
465+
{
466+
try
467+
{
468+
return await stream.SendGoAway(id);
469+
}
470+
catch
457471
{
458-
return OutboundControlStream.SendGoAway(id);
472+
// The control stream may not be healthy.
473+
// Ignore error sending go away.
459474
}
460475
}
476+
461477
return default;
462478
}
463479

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
99
internal class Http3ConnectionErrorException : Exception
1010
{
1111
public Http3ConnectionErrorException(string message, Http3ErrorCode errorCode)
12-
: base($"HTTP/3 connection error ({errorCode}): {message}")
12+
: base($"HTTP/3 connection error ({Http3Formatting.ToFormattedErrorCode(errorCode)}): {message}")
1313
{
1414
ErrorCode = errorCode;
1515
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,15 @@ await WaitForConnectionErrorAsync<Http3ConnectionErrorException>(
160160
[Fact]
161161
public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError()
162162
{
163-
OnCreateServerControlStream = () => throw new Exception();
163+
OnCreateServerControlStream = () =>
164+
{
165+
var controlStream = new Http3ControlStream(this, StreamInitiator.Server);
166+
167+
// Make server connection error when trying to write to control stream.
168+
controlStream.StreamContext.Transport.Output.Complete();
169+
170+
return controlStream;
171+
};
164172

165173
await InitializeConnectionAsync(_noopApplication);
166174

src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,16 @@ internal async ValueTask<Http3ControlStream> GetInboundControlStream()
187187
if (_inboundControlStream == null)
188188
{
189189
var reader = MultiplexedConnectionContext.ToClientAcceptQueue.Reader;
190-
while (await reader.WaitToReadAsync())
190+
while (await reader.WaitToReadAsync().DefaultTimeout())
191191
{
192192
while (reader.TryRead(out var stream))
193193
{
194194
_inboundControlStream = stream;
195195
var streamId = await stream.TryReadStreamIdAsync();
196-
Debug.Assert(streamId == 0, "StreamId sent that was non-zero, which isn't handled by tests");
196+
197+
// -1 means stream was completed.
198+
Debug.Assert(streamId == 0 || streamId == -1, "StreamId sent that was non-zero, which isn't handled by tests");
199+
197200
return _inboundControlStream;
198201
}
199202
}
@@ -231,6 +234,12 @@ internal async Task WaitForConnectionErrorAsync<TException>(bool ignoreNonGoAway
231234
}
232235

233236
AssertConnectionError<TException>(expectedErrorCode, expectedErrorMessage);
237+
238+
// Verify HttpConnection.ProcessRequestsAsync has exited.
239+
await _connectionTask.DefaultTimeout();
240+
241+
// Verify server-to-client control stream has completed.
242+
await _inboundControlStream.ReceiveEndAsync();
234243
}
235244

236245
internal void AssertConnectionError<TException>(Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage) where TException : Exception

0 commit comments

Comments
 (0)