diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index f755e96f072c..66e0bff77f2c 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -683,4 +683,7 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
Request stream ended without headers.
+
+ Reading the control stream header timed out.
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
index b6ead9433d5b..db9be53ed3a6 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
@@ -14,14 +15,15 @@
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
- internal class Http3Connection : ITimeoutHandler
+ internal class Http3Connection : ITimeoutHandler, IHttp3StreamLifetimeHandler
{
- internal readonly Dictionary _streams = new Dictionary();
+ internal readonly Dictionary _streams = new Dictionary();
private long _highestOpenedStreamId;
private readonly object _sync = new object();
@@ -84,9 +86,6 @@ public async Task ProcessStreamsAsync(IHttpApplication httpA
{
try
{
- // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs.
- _timeoutControl.Initialize(_systemClock.UtcNowTicks);
-
var connectionHeartbeatFeature = _context.ConnectionFeatures.Get();
var connectionLifetimeNotificationFeature = _context.ConnectionFeatures.Get();
@@ -198,11 +197,42 @@ public void Tick()
return;
}
- // It's safe to use UtcNowUnsynchronized since Tick is called by the Heartbeat.
- var now = _systemClock.UtcNowUnsynchronized;
- _timeoutControl.Tick(now);
+ UpdateStartingStreams();
+ }
+
+ private void UpdateStartingStreams()
+ {
+ var now = _systemClock.UtcNow.Ticks;
+
+ lock (_streams)
+ {
+ foreach (var stream in _streams.Values)
+ {
+ if (stream.ReceivedHeader)
+ {
+ continue;
+ }
+
+ if (stream.HeaderTimeoutTicks == default)
+ {
+ // On expiration overflow, use max value.
+ var expirationTicks = now + _context.ServiceContext.ServerOptions.Limits.RequestHeadersTimeout.Ticks;
+ stream.HeaderTimeoutTicks = expirationTicks >= 0 ? expirationTicks : long.MaxValue;
+ }
- // TODO cancel process stream loop to update logic.
+ if (stream.HeaderTimeoutTicks < now)
+ {
+ if (stream.IsRequestStream)
+ {
+ stream.Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http3ErrorCode.RequestRejected);
+ }
+ else
+ {
+ stream.Abort(new ConnectionAbortedException(CoreStrings.Http3ControlStreamHeaderTimeout), Http3ErrorCode.StreamCreationError);
+ }
+ }
+ }
+ }
}
public void OnTimeout(TimeoutReason reason)
@@ -213,13 +243,11 @@ public void OnTimeout(TimeoutReason reason)
// TODO what timeouts should we handle here? Is keep alive something we should care about?
switch (reason)
{
- case TimeoutReason.KeepAlive:
- SendGoAway(GetHighestStreamId()).Preserve();
- break;
case TimeoutReason.TimeoutFeature:
SendGoAway(GetHighestStreamId()).Preserve();
break;
- case TimeoutReason.RequestHeaders:
+ case TimeoutReason.RequestHeaders: // Request header timeout is handled in starting stream queue
+ case TimeoutReason.KeepAlive: // Keep-alive is handled by msquic
case TimeoutReason.ReadDataRate:
case TimeoutReason.WriteDataRate:
case TimeoutReason.RequestBodyDrain:
@@ -245,8 +273,6 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication(IHttpApplication(application, this, httpConnectionContext);
+ var stream = new Http3ControlStream(application, httpConnectionContext);
+ lock (_streams)
+ {
+ _streams[streamId] = stream;
+ }
+
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
}
else
{
- var streamId = streamIdFeature.StreamId;
-
+ // Request stream
UpdateHighestStreamId(streamId);
- var http3Stream = new Http3Stream(application, this, httpConnectionContext);
- var stream = http3Stream;
+ var stream = new Http3Stream(application, httpConnectionContext);
lock (_streams)
{
_activeRequestCount++;
- _streams[streamId] = http3Stream;
+ _streams[streamId] = stream;
}
+
KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3);
ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false);
}
@@ -363,8 +396,6 @@ internal async Task InnerProcessStreamsAsync(IHttpApplication CreateNewUnidirectionalStreamAsync(application, this, httpConnectionContext);
+ return new Http3ControlStream(application, httpConnectionContext);
}
private ValueTask SendGoAway(long id)
@@ -466,33 +487,10 @@ private ValueTask SendGoAway(long id)
return OutboundControlStream.SendGoAway(id);
}
}
- return new ValueTask();
+ return default;
}
- public void ApplyMaxHeaderListSize(long value)
- {
- }
-
- internal void ApplyBlockedStream(long value)
- {
- }
-
- internal void ApplyMaxTableCapacity(long value)
- {
- }
-
- internal void RemoveStream(long streamId)
- {
- lock (_streams)
- {
- _activeRequestCount--;
- _streams.Remove(streamId);
- }
-
- _streamCompletionAwaitable.Complete();
- }
-
- public bool SetInboundControlStream(Http3ControlStream stream)
+ public bool OnInboundControlStream(Http3ControlStream stream)
{
lock (_sync)
{
@@ -505,7 +503,7 @@ public bool SetInboundControlStream(Http3ControlStream stream)
}
}
- public bool SetInboundEncoderStream(Http3ControlStream stream)
+ public bool OnInboundEncoderStream(Http3ControlStream stream)
{
lock (_sync)
{
@@ -518,7 +516,7 @@ public bool SetInboundEncoderStream(Http3ControlStream stream)
}
}
- public bool SetInboundDecoderStream(Http3ControlStream stream)
+ public bool OnInboundDecoderStream(Http3ControlStream stream)
{
lock (_sync)
{
@@ -531,6 +529,38 @@ public bool SetInboundDecoderStream(Http3ControlStream stream)
}
}
+ public void OnStreamCompleted(IHttp3Stream stream)
+ {
+ lock (_streams)
+ {
+ _activeRequestCount--;
+ _streams.Remove(stream.StreamId);
+ }
+
+ _streamCompletionAwaitable.Complete();
+ }
+
+ public void OnStreamConnectionError(Http3ConnectionErrorException ex)
+ {
+ Log.Http3ConnectionError(ConnectionId, ex);
+ Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
+ }
+
+ public void OnInboundControlStreamSetting(Http3SettingType type, long value)
+ {
+ switch (type)
+ {
+ case Http3SettingType.QPackMaxTableCapacity:
+ break;
+ case Http3SettingType.MaxFieldSectionSize:
+ break;
+ case Http3SettingType.QPackBlockedStreams:
+ break;
+ default:
+ throw new InvalidOperationException("Unexpected setting: " + type);
+ }
+ }
+
private static class GracefulCloseInitiator
{
public const int None = 0;
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
index 39dce967bde6..9e8fcfb2d136 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs
@@ -17,32 +17,34 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
- internal abstract class Http3ControlStream : IThreadPoolWorkItem
+ internal abstract class Http3ControlStream : IHttp3Stream, IThreadPoolWorkItem
{
- private const int ControlStream = 0;
- private const int EncoderStream = 2;
- private const int DecoderStream = 3;
+ private const int ControlStreamTypeId = 0;
+ private const int EncoderStreamTypeId = 2;
+ private const int DecoderStreamTypeId = 3;
private readonly Http3FrameWriter _frameWriter;
- private readonly Http3Connection _http3Connection;
private readonly Http3StreamContext _context;
private readonly Http3PeerSettings _serverPeerSettings;
private readonly IStreamIdFeature _streamIdFeature;
- private readonly IProtocolErrorCodeFeature _protocolErrorCodeFeature;
+ private readonly IProtocolErrorCodeFeature _errorCodeFeature;
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
private volatile int _isClosed;
private int _gracefulCloseInitiator;
+ private long _headerType;
private bool _haveReceivedSettingsFrame;
- public Http3ControlStream(Http3Connection http3Connection, Http3StreamContext context)
+ public long StreamId => _streamIdFeature.StreamId;
+
+ public Http3ControlStream(Http3StreamContext context)
{
var httpLimits = context.ServiceContext.ServerOptions.Limits;
- _http3Connection = http3Connection;
_context = context;
_serverPeerSettings = context.ServerSettings;
_streamIdFeature = context.ConnectionFeatures.Get()!;
- _protocolErrorCodeFeature = context.ConnectionFeatures.Get()!;
+ _errorCodeFeature = context.ConnectionFeatures.Get()!;
+ _headerType = -1;
_frameWriter = new Http3FrameWriter(
context.Transport.Output,
@@ -57,27 +59,28 @@ public Http3ControlStream(Http3Connection http3Connection, Http3StreamContext co
private void OnStreamClosed()
{
- Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM"));
+ Abort(new ConnectionAbortedException("HTTP_CLOSED_CRITICAL_STREAM"), Http3ErrorCode.InternalError);
}
public PipeReader Input => _context.Transport.Input;
public IKestrelTrace Log => _context.ServiceContext.Log;
- public void Abort(ConnectionAbortedException ex)
- {
+ public long HeaderTimeoutTicks { get; set; }
+ public bool ReceivedHeader => _headerType >= 0;
- }
+ public bool IsRequestStream => false;
- public void HandleReadDataRateTimeout()
+ public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode)
{
- //Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond);
- Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout));
- }
+ // TODO - Should there be a check here to track abort state to avoid
+ // running twice for a request?
- public void HandleRequestHeadersTimeout()
- {
- //Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout));
- Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout));
+ Log.Http3StreamAbort(_context.ConnectionId, errorCode, abortReason);
+
+ _errorCodeFeature.Error = (long)errorCode;
+ _frameWriter.Abort(abortReason);
+
+ Input.Complete(abortReason);
}
public void OnInputOrOutputCompleted()
@@ -111,8 +114,9 @@ internal async ValueTask SendSettingsFrameAsync()
await _frameWriter.WriteSettingsAsync(_serverPeerSettings.GetNonProtocolDefaults());
}
- private async ValueTask TryReadStreamIdAsync()
+ private async ValueTask TryReadStreamHeaderAsync()
{
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2
while (_isClosed == 0)
{
var result = await Input.ReadAsync();
@@ -149,51 +153,47 @@ public async Task ProcessRequestAsync(IHttpApplication appli
{
try
{
- var streamType = await TryReadStreamIdAsync();
-
- if (streamType == -1)
- {
- return;
- }
+ _headerType = await TryReadStreamHeaderAsync();
- if (streamType == ControlStream)
+ switch (_headerType)
{
- if (!_http3Connection.SetInboundControlStream(this))
- {
- // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1
- throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError);
- }
+ case -1:
+ return;
+ case ControlStreamTypeId:
+ if (!_context.StreamLifetimeHandler.OnInboundControlStream(this))
+ {
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1
+ throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError);
+ }
- await HandleControlStream();
- }
- else if (streamType == EncoderStream)
- {
- if (!_http3Connection.SetInboundEncoderStream(this))
- {
- // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
- throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError);
- }
+ await HandleControlStream();
+ break;
+ case EncoderStreamTypeId:
+ if (!_context.StreamLifetimeHandler.OnInboundEncoderStream(this))
+ {
+ // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
+ throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError);
+ }
- await HandleEncodingDecodingTask();
- }
- else if (streamType == DecoderStream)
- {
- if (!_http3Connection.SetInboundDecoderStream(this))
- {
- // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
- throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError);
- }
- await HandleEncodingDecodingTask();
- }
- else
- {
- // TODO Close the control stream as it's unexpected.
+ await HandleEncodingDecodingTask();
+ break;
+ case DecoderStreamTypeId:
+ if (!_context.StreamLifetimeHandler.OnInboundDecoderStream(this))
+ {
+ // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2
+ throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError);
+ }
+ await HandleEncodingDecodingTask();
+ break;
+ default:
+ // TODO Close the control stream as it's unexpected.
+ break;
}
}
catch (Http3ConnectionErrorException ex)
{
- Log.Http3ConnectionError(_http3Connection.ConnectionId, ex);
- _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
+ _errorCodeFeature.Error = (long)ex.ErrorCode;
+ _context.StreamLifetimeHandler.OnStreamConnectionError(ex);
}
}
@@ -227,10 +227,8 @@ private async Task HandleControlStream()
}
catch (Http3ConnectionErrorException ex)
{
- _protocolErrorCodeFeature.Error = (long)ex.ErrorCode;
-
- Log.Http3ConnectionError(_http3Connection.ConnectionId, ex);
- _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
+ _errorCodeFeature.Error = (long)ex.ErrorCode;
+ _context.StreamLifetimeHandler.OnStreamConnectionError(ex);
}
finally
{
@@ -324,13 +322,9 @@ private void ProcessSetting(long id, long value)
var message = CoreStrings.FormatHttp3ErrorControlStreamReservedSetting("0x" + id.ToString("X", CultureInfo.InvariantCulture));
throw new Http3ConnectionErrorException(message, Http3ErrorCode.SettingsError);
case (long)Http3SettingType.QPackMaxTableCapacity:
- _http3Connection.ApplyMaxTableCapacity(value);
- break;
case (long)Http3SettingType.MaxFieldSectionSize:
- _http3Connection.ApplyMaxHeaderListSize(value);
- break;
case (long)Http3SettingType.QPackBlockedStreams:
- _http3Connection.ApplyBlockedStream(value);
+ _context.StreamLifetimeHandler.OnInboundControlStreamSetting((Http3SettingType)id, value);
break;
default:
// Ignore all unknown settings.
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs
index 3b407bc3e81b..630b74b00a44 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStreamOfT.cs
@@ -10,7 +10,7 @@ internal sealed class Http3ControlStream : Http3ControlStream, IHostCo
{
private readonly IHttpApplication _application;
- public Http3ControlStream(IHttpApplication application, Http3Connection connection, Http3StreamContext context) : base(connection, context)
+ public Http3ControlStream(IHttpApplication application, Http3StreamContext context) : base(context)
{
_application = application;
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 899b464d7f3d..a5768a2eb637 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -20,7 +20,7 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
{
- internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler, IThreadPoolWorkItem, ITimeoutHandler, IRequestProcessor
+ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpHeadersHandler, IThreadPoolWorkItem, ITimeoutHandler, IRequestProcessor
{
private static ReadOnlySpan AuthorityBytes => new byte[10] { (byte)':', (byte)'a', (byte)'u', (byte)'t', (byte)'h', (byte)'o', (byte)'r', (byte)'i', (byte)'t', (byte)'y' };
private static ReadOnlySpan MethodBytes => new byte[7] { (byte)':', (byte)'m', (byte)'e', (byte)'t', (byte)'h', (byte)'o', (byte)'d' };
@@ -48,21 +48,16 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler,
private int _totalParsedHeaderSize;
private bool _isMethodConnect;
- private readonly Http3Connection _http3Connection;
private TaskCompletionSource? _appCompleted;
public Pipe RequestBodyPipe { get; }
- public Http3Stream(Http3Connection http3Connection, Http3StreamContext context)
+ public Http3Stream(Http3StreamContext context)
{
Initialize(context);
InputRemaining = null;
- // First, determine how we know if an Http3stream is unidirectional or bidirectional
- var httpLimits = context.ServiceContext.ServerOptions.Limits;
- var http3Limits = httpLimits.Http3;
- _http3Connection = http3Connection;
_context = context;
_errorCodeFeature = _context.ConnectionFeatures.Get()!;
@@ -72,7 +67,7 @@ public Http3Stream(Http3Connection http3Connection, Http3StreamContext context)
context.Transport.Output,
context.StreamContext,
context.TimeoutControl,
- httpLimits.MinResponseDataRate,
+ context.ServiceContext.ServerOptions.Limits.MinResponseDataRate,
context.ConnectionId,
context.MemoryPool,
context.ServiceContext.Log,
@@ -99,6 +94,12 @@ public Http3Stream(Http3Connection http3Connection, Http3StreamContext context)
public ISystemClock SystemClock => _context.ServiceContext.SystemClock;
public KestrelServerLimits Limits => _context.ServiceContext.ServerOptions.Limits;
+ public long StreamId => _streamIdFeature.StreamId;
+
+ public long HeaderTimeoutTicks { get; set; }
+ public bool ReceivedHeader => _appCompleted != null; // TCS is assigned once headers are received
+
+ public bool IsRequestStream => true;
public void Abort(ConnectionAbortedException ex)
{
@@ -406,10 +407,7 @@ public async Task ProcessRequestAsync(IHttpApplication appli
error = ex;
_errorCodeFeature.Error = (long)ex.ErrorCode;
- Log.Http3ConnectionError(_http3Connection.ConnectionId, ex);
- _http3Connection.Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode);
-
- // TODO: HTTP/3 stream will be aborted by connection. Check this is correct.
+ _context.StreamLifetimeHandler.OnStreamConnectionError(ex);
}
catch (Exception ex)
{
@@ -442,7 +440,7 @@ public async Task ProcessRequestAsync(IHttpApplication appli
{
await _context.StreamContext.DisposeAsync();
- _http3Connection.RemoveStream(_streamIdFeature.StreamId);
+ _context.StreamLifetimeHandler.OnStreamCompleted(this);
}
}
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs
index e88de7400f4e..63a25e94f485 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs
@@ -11,7 +11,7 @@ class Http3Stream : Http3Stream, IHostContextContainer where
{
private readonly IHttpApplication _application;
- public Http3Stream(IHttpApplication application, Http3Connection connection, Http3StreamContext context) : base(connection, context)
+ public Http3Stream(IHttpApplication application, Http3StreamContext context) : base(context)
{
_application = application;
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs
new file mode 100644
index 000000000000..5cdeb8301e21
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3Stream.cs
@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Net.Http;
+using Microsoft.AspNetCore.Connections;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
+{
+ internal interface IHttp3Stream
+ {
+ ///
+ /// The stream ID is set by QUIC.
+ ///
+ long StreamId { get; }
+
+ ///
+ /// Used to track the timeout between when the stream was started by the client, and getting a header.
+ /// Value is driven by .
+ ///
+ long HeaderTimeoutTicks { get; set; }
+
+ ///
+ /// The stream has received and parsed the header frame.
+ /// - Request streams = HEADERS frame.
+ /// - Control streams = unidirectional stream header.
+ ///
+ bool ReceivedHeader { get; }
+
+ bool IsRequestStream { get; }
+
+ void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode);
+ }
+}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3StreamLifetimeHandler.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3StreamLifetimeHandler.cs
new file mode 100644
index 000000000000..57bbbf99c597
--- /dev/null
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/IHttp3StreamLifetimeHandler.cs
@@ -0,0 +1,16 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
+{
+ internal interface IHttp3StreamLifetimeHandler
+ {
+ void OnStreamCompleted(IHttp3Stream stream);
+ void OnStreamConnectionError(Http3ConnectionErrorException ex);
+
+ bool OnInboundControlStream(Http3ControlStream stream);
+ bool OnInboundEncoderStream(Http3ControlStream stream);
+ bool OnInboundDecoderStream(Http3ControlStream stream);
+ void OnInboundControlStreamSetting(Http3SettingType type, long value);
+ }
+}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs
index 8cd1cc177130..5f8130e2413d 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3ConnectionContext.cs
@@ -1,10 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Buffers;
using System.Net;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs
index dcb4a2c0492e..bb01e49d4baa 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs
@@ -22,13 +22,16 @@ public Http3StreamContext(
IPEndPoint? localEndPoint,
IPEndPoint? remoteEndPoint,
IDuplexPipe transport,
+ IHttp3StreamLifetimeHandler streamLifetimeHandler,
ConnectionContext streamContext,
Http3PeerSettings settings) : base(connectionId, protocols, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint, transport)
{
+ StreamLifetimeHandler = streamLifetimeHandler;
StreamContext = streamContext;
ServerSettings = settings;
}
+ public IHttp3StreamLifetimeHandler StreamLifetimeHandler { get; }
public ConnectionContext StreamContext { get; }
public Http3PeerSettings ServerSettings { get; }
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs
index 65ed15f0179a..876828f349d1 100644
--- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs
@@ -74,7 +74,7 @@ public async Task ProcessRequestsAsync(IHttpApplication http
default:
// SelectProtocol() only returns Http1, Http2 or None.
- throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2, Http3 or None.");
+ throw new NotSupportedException($"{nameof(SelectProtocol)} returned something other than Http1, Http2 or None.");
}
_requestProcessor = requestProcessor;
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs
index 6f303d56b218..f45577fb6287 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ITimeoutControl.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
@@ -14,6 +15,8 @@ internal interface ITimeoutControl
void CancelTimeout();
void InitializeHttp2(InputFlowControl connectionInputFlowControl);
+ void Tick(DateTimeOffset now);
+
void StartRequestBody(MinDataRate minRate);
void StopRequestBody();
void StartTimingRead();
diff --git a/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs
index 1314e1699787..d1474418ca23 100644
--- a/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs
+++ b/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs
@@ -18,11 +18,9 @@ public class Http3HttpProtocolFeatureCollectionTests
public Http3HttpProtocolFeatureCollectionTests()
{
- var connection = new Http3Connection(TestContextFactory.CreateHttp3ConnectionContext());
-
var streamContext = TestContextFactory.CreateHttp3StreamContext(transport: DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()).Application);
- var http3Stream = new TestHttp3Stream(connection, streamContext);
+ var http3Stream = new TestHttp3Stream(streamContext);
http3Stream.Reset();
_http3Collection = http3Stream;
}
@@ -61,7 +59,7 @@ public void Http3StreamFeatureCollectionDoesIncludeIHttpMinRequestBodyDataRateFe
private class TestHttp3Stream : Http3Stream
{
- public TestHttp3Stream(Http3Connection connection, Http3StreamContext context) : base(connection, context)
+ public TestHttp3Stream(Http3StreamContext context) : base(context)
{
}
diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs
index 115ba593ef38..1f0885cedd52 100644
--- a/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs
+++ b/src/Servers/Kestrel/perf/Microbenchmarks/Mocks/MockTimeoutControl.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
@@ -58,5 +59,9 @@ public void StopTimingRead()
public void StopTimingWrite()
{
}
+
+ public void Tick(DateTimeOffset now)
+ {
+ }
}
}
diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs
index a50e672c0bf1..6fc84e150970 100644
--- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs
+++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs
@@ -15,6 +15,7 @@
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.FlowControl;
+using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
@@ -171,7 +172,8 @@ public static Http3StreamContext CreateHttp3StreamContext(
IPEndPoint localEndPoint = null,
IPEndPoint remoteEndPoint = null,
IDuplexPipe transport = null,
- ITimeoutControl timeoutControl = null)
+ ITimeoutControl timeoutControl = null,
+ IHttp3StreamLifetimeHandler streamLifetimeHandler = null)
{
var context = new Http3StreamContext
(
@@ -184,6 +186,7 @@ public static Http3StreamContext CreateHttp3StreamContext(
localEndPoint: localEndPoint,
remoteEndPoint: remoteEndPoint,
transport: transport,
+ streamLifetimeHandler: streamLifetimeHandler,
streamContext: null,
settings: null
);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
index ed710fdc6b61..e11b5d8e2262 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs
@@ -432,8 +432,6 @@ void IHttpHeadersHandler.OnHeadersComplete(bool endStream) { }
protected void CreateConnection()
{
- var limits = _serviceContext.ServerOptions.Limits;
-
// Always dispatch test code back to the ThreadPool. This prevents deadlocks caused by continuing
// Http2Connection.ProcessRequestsAsync() loop with writer locks acquired. Run product code inline to make
// it easier to verify request frames are processed correctly immediately after sending the them.
@@ -1381,6 +1379,11 @@ public virtual void BytesWrittenToBuffer(MinDataRate minRate, long size)
{
_realTimeoutControl.BytesWrittenToBuffer(minRate, size);
}
+
+ public virtual void Tick(DateTimeOffset now)
+ {
+ _realTimeoutControl.Tick(now);
+ }
}
}
}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index a8266f60d566..5c89c5577e0f 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -1823,7 +1823,7 @@ await requestStream.WaitForStreamErrorAsync(
await WaitForConnectionErrorAsync(
ignoreNonGoAwayFrames: true,
- expectedLastStreamId: 0,
+ expectedLastStreamId: 8,
expectedErrorCode: Http3ErrorCode.UnexpectedFrame,
expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType));
}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
index 0c3c1c5980f3..443adc786522 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
@@ -33,7 +33,7 @@
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
- public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable
+ public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable
{
protected static readonly int MaxRequestHeaderFieldSize = 16 * 1024;
protected static readonly string _4kHeaderValue = new string('a', 4096);
@@ -55,6 +55,7 @@ public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable
protected readonly RequestDelegate _echoHost;
private Http3ControlStream _inboundControlStream;
+ private long _currentStreamId;
public Http3TestBase()
{
@@ -119,6 +120,15 @@ public override void Initialize(TestContext context, MethodInfo methodInfo, obje
};
}
+ internal long GetStreamId(long mask)
+ {
+ var id = (_currentStreamId << 2) | mask;
+
+ _currentStreamId += 1;
+
+ return id;
+ }
+
internal async ValueTask GetInboundControlStream()
{
if (_inboundControlStream == null)
@@ -136,7 +146,7 @@ internal async ValueTask GetInboundControlStream()
}
}
- return null;
+ return _inboundControlStream;
}
internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage)
@@ -172,6 +182,27 @@ internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamI
Assert.Equal(expectedLastStreamId, streamId);
}
+ protected void AdvanceClock(TimeSpan timeSpan)
+ {
+ var clock = _serviceContext.MockSystemClock;
+ var endTime = clock.UtcNow + timeSpan;
+
+ while (clock.UtcNow + Heartbeat.Interval < endTime)
+ {
+ clock.UtcNow += Heartbeat.Interval;
+ _timeoutControl.Tick(clock.UtcNow);
+ }
+
+ clock.UtcNow = endTime;
+ _timeoutControl.Tick(clock.UtcNow);
+ }
+
+ protected void TriggerTick(DateTimeOffset now)
+ {
+ _serviceContext.MockSystemClock.UtcNow = now;
+ Connection?.Tick();
+ }
+
protected async Task InitializeConnectionAsync(RequestDelegate application)
{
if (Connection == null)
@@ -198,9 +229,6 @@ internal async ValueTask InitializeConnectionAndStreamsAsync
protected void CreateConnection()
{
- var limits = _serviceContext.ServerOptions.Limits;
-
-
MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this);
var httpConnectionContext = new Http3ConnectionContext(
@@ -263,11 +291,14 @@ public ValueTask CreateControlStream()
return CreateControlStream(id: 0);
}
- public async ValueTask CreateControlStream(int id)
+ public async ValueTask CreateControlStream(int? id)
{
- var stream = new Http3ControlStream(this);
+ var stream = new Http3ControlStream(this, StreamInitiator.Client);
MultiplexedConnectionContext.ToServerAcceptQueue.Writer.TryWrite(stream.StreamContext);
- await stream.WriteStreamIdAsync(id);
+ if (id != null)
+ {
+ await stream.WriteStreamIdAsync(id.GetValueOrDefault());
+ }
return stream;
}
@@ -285,12 +316,13 @@ public ValueTask StartBidirectionalStreamAsync()
return new ValueTask(stream.StreamContext);
}
- public class Http3StreamBase
+ public class Http3StreamBase : IProtocolErrorCodeFeature
{
internal DuplexPipe.DuplexPipePair _pair;
internal Http3TestBase _testBase;
internal Http3Connection _connection;
private long _bytesReceived;
+ public long Error { get; set; }
protected Task SendAsync(ReadOnlySpan span)
{
@@ -361,21 +393,35 @@ internal Task EndStreamAsync()
{
return _pair.Application.Output.CompleteAsync().AsTask();
}
+
+ internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string expectedErrorMessage)
+ {
+ var readResult = await _pair.Application.Input.ReadAsync().DefaultTimeout();
+ _testBase.Logger.LogTrace("Input is completed");
+
+ Assert.True(readResult.IsCompleted);
+ Assert.Equal(protocolError, (Http3ErrorCode)Error);
+
+ if (expectedErrorMessage != null)
+ {
+ Assert.Contains(_testBase.LogMessages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false);
+ }
+ }
}
- internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IProtocolErrorCodeFeature
+ internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler
{
private TestStreamContext _testStreamContext;
+ private long _streamId;
internal ConnectionContext StreamContext { get; }
public bool CanRead => true;
public bool CanWrite => true;
- public long StreamId => 0;
+ public long StreamId => _streamId;
public bool Disposed => _testStreamContext.Disposed;
- public long Error { get; set; }
private readonly byte[] _headerEncodingBuffer = new byte[64 * 1024];
private QPackEncoder _qpackEncoder = new QPackEncoder();
@@ -390,7 +436,8 @@ public Http3RequestStream(Http3TestBase testBase, Http3Connection connection)
var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
- _testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this);
+ _streamId = testBase.GetStreamId(0x00);
+ _testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this, _streamId);
StreamContext = _testStreamContext;
}
@@ -405,6 +452,17 @@ public async Task SendHeadersAsync(IEnumerable> hea
await SendFrameAsync(frame, buffer.Slice(0, length), endStream);
}
+ internal async Task SendHeadersPartialAsync()
+ {
+ // Send HEADERS frame header without content.
+ var outputWriter = _pair.Application.Output;
+ var frame = new Http3RawFrame();
+ frame.PrepareData();
+ frame.Length = 10;
+ Http3FrameWriter.WriteHeader(frame, outputWriter);
+ await SendAsync(Span.Empty);
+ }
+
internal async Task SendDataAsync(Memory data, bool endStream = false)
{
var frame = new Http3RawFrame();
@@ -454,20 +512,6 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan value)
{
_decodedHeaders[((Span)H3StaticTable.GetHeaderFieldAt(index).Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters();
}
-
- internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, string expectedErrorMessage)
- {
- var readResult = await _pair.Application.Input.ReadAsync().DefaultTimeout();
- _testBase.Logger.LogTrace("Input is completed");
-
- Assert.True(readResult.IsCompleted);
- Assert.Equal(protocolError, (Http3ErrorCode)Error);
-
- if (expectedErrorMessage != null)
- {
- Assert.Contains(_testBase.LogMessages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false);
- }
- }
}
internal class Http3FrameWithPayload : Http3RawFrame
@@ -482,24 +526,30 @@ public Http3FrameWithPayload() : base()
public ReadOnlySequence PayloadSequence => new ReadOnlySequence(Payload);
}
- public class Http3ControlStream : Http3StreamBase, IProtocolErrorCodeFeature
+ public enum StreamInitiator
+ {
+ Client,
+ Server
+ }
+
+ public class Http3ControlStream : Http3StreamBase
{
internal ConnectionContext StreamContext { get; }
+ private long _streamId;
public bool CanRead => true;
public bool CanWrite => false;
- public long StreamId => 0;
-
- public long Error { get; set; }
+ public long StreamId => _streamId;
- public Http3ControlStream(Http3TestBase testBase)
+ public Http3ControlStream(Http3TestBase testBase, StreamInitiator initiator)
{
_testBase = testBase;
var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool);
_pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions);
- StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this);
+ _streamId = testBase.GetStreamId(initiator == StreamInitiator.Client ? 0x02 : 0x03);
+ StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this, _streamId);
}
public Http3ControlStream(ConnectionContext streamContext)
@@ -507,6 +557,12 @@ public Http3ControlStream(ConnectionContext streamContext)
StreamContext = streamContext;
}
+ internal async Task ExpectSettingsAsync()
+ {
+ var http3WithPayload = await ReceiveFrameAsync();
+ Assert.Equal(Http3FrameType.Settings, http3WithPayload.Type);
+ }
+
public async Task WriteStreamIdAsync(int id)
{
var writableBuffer = _pair.Application.Output;
@@ -654,7 +710,7 @@ public override async ValueTask AcceptAsync(CancellationToken
public override ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default)
{
- var stream = new Http3ControlStream(_testBase);
+ var stream = new Http3ControlStream(_testBase, StreamInitiator.Server);
ToClientAcceptQueue.Writer.WriteAsync(stream);
return new ValueTask(stream.StreamContext);
}
@@ -672,16 +728,17 @@ public void RequestClose()
private class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature
{
private DuplexPipePair _pair;
- public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature feature)
+ public TestStreamContext(bool canRead, bool canWrite, DuplexPipePair pair, IProtocolErrorCodeFeature errorCodeFeature, long streamId)
{
_pair = pair;
Features = new FeatureCollection();
Features.Set(this);
Features.Set(this);
- Features.Set(feature);
+ Features.Set(errorCodeFeature);
CanRead = canRead;
CanWrite = canWrite;
+ StreamId = streamId;
}
public bool Disposed { get; private set; }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs
new file mode 100644
index 000000000000..86922da93e77
--- /dev/null
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs
@@ -0,0 +1,215 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
+{
+ public class Http3TimeoutTests : Http3TestBase
+ {
+ [Fact]
+ public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_StreamError()
+ {
+ var now = _serviceContext.MockSystemClock.UtcNow;
+ var limits = _serviceContext.ServerOptions.Limits;
+
+ var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout();
+
+ var controlStream = await GetInboundControlStream().DefaultTimeout();
+ await controlStream.ExpectSettingsAsync().DefaultTimeout();
+
+ await AssertIsTrueRetryAsync(
+ () => Connection._streams.Count == 2,
+ "Wait until streams have been created.").DefaultTimeout();
+
+ var serverRequestStream = Connection._streams[requestStream.StreamId];
+
+ await requestStream.SendHeadersPartialAsync().DefaultTimeout();
+
+ TriggerTick(now);
+ TriggerTick(now + limits.RequestHeadersTimeout);
+
+ Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.HeaderTimeoutTicks);
+
+ TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1));
+
+ await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.RequestRejected, CoreStrings.BadRequest_RequestHeadersTimeout);
+ }
+
+ [Fact]
+ public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success()
+ {
+ var now = _serviceContext.MockSystemClock.UtcNow;
+ var limits = _serviceContext.ServerOptions.Limits;
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "Custom"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+
+ var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout();
+
+ var controlStream = await GetInboundControlStream().DefaultTimeout();
+ await controlStream.ExpectSettingsAsync().DefaultTimeout();
+
+ await AssertIsTrueRetryAsync(
+ () => Connection._streams.Count == 2,
+ "Wait until streams have been created.").DefaultTimeout();
+
+ var serverRequestStream = Connection._streams[requestStream.StreamId];
+
+ TriggerTick(now);
+ TriggerTick(now + limits.RequestHeadersTimeout);
+
+ Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.HeaderTimeoutTicks);
+
+ await requestStream.SendHeadersAsync(headers).DefaultTimeout();
+
+ await AssertIsTrueRetryAsync(
+ () => serverRequestStream.ReceivedHeader,
+ "Request stream has read headers.").DefaultTimeout();
+
+ TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1));
+
+ await requestStream.SendDataAsync(Memory.Empty, endStream: true);
+
+ await requestStream.ExpectReceiveEndOfStream();
+ }
+
+ [Fact]
+ public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_StreamError()
+ {
+ var now = _serviceContext.MockSystemClock.UtcNow;
+ var limits = _serviceContext.ServerOptions.Limits;
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "Custom"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+
+ await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
+
+ var controlStream = await GetInboundControlStream().DefaultTimeout();
+ await controlStream.ExpectSettingsAsync().DefaultTimeout();
+
+ var outboundControlStream = await CreateControlStream(id: null);
+
+ await AssertIsTrueRetryAsync(
+ () => Connection._streams.Count == 1,
+ "Wait until streams have been created.").DefaultTimeout();
+
+ var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId];
+
+ TriggerTick(now);
+ TriggerTick(now + limits.RequestHeadersTimeout);
+
+ Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverInboundControlStream.HeaderTimeoutTicks);
+
+ TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1));
+
+ await outboundControlStream.WaitForStreamErrorAsync(Http3ErrorCode.StreamCreationError, CoreStrings.Http3ControlStreamHeaderTimeout);
+ }
+
+ [Fact]
+ public async Task ControlStream_HeaderReceivedWithinRequestHeadersTimeout_StreamError()
+ {
+ var now = _serviceContext.MockSystemClock.UtcNow;
+ var limits = _serviceContext.ServerOptions.Limits;
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "Custom"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+
+ await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
+
+ var controlStream = await GetInboundControlStream().DefaultTimeout();
+ await controlStream.ExpectSettingsAsync().DefaultTimeout();
+
+ var outboundControlStream = await CreateControlStream(id: null);
+
+ await AssertIsTrueRetryAsync(
+ () => Connection._streams.Count == 1,
+ "Wait until streams have been created.").DefaultTimeout();
+
+ var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId];
+
+ TriggerTick(now);
+ TriggerTick(now + limits.RequestHeadersTimeout);
+
+ await outboundControlStream.WriteStreamIdAsync(id: 0);
+
+ await AssertIsTrueRetryAsync(
+ () => serverInboundControlStream.ReceivedHeader,
+ "Control stream has read header.").DefaultTimeout();
+
+ TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1));
+ }
+
+ [Fact]
+ public async Task ControlStream_RequestHeadersTimeoutMaxValue_ExpirationIsMaxValue()
+ {
+ var now = _serviceContext.MockSystemClock.UtcNow;
+ var limits = _serviceContext.ServerOptions.Limits;
+ limits.RequestHeadersTimeout = TimeSpan.MaxValue;
+
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "Custom"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Authority, "localhost:80"),
+ };
+
+ await InitializeConnectionAsync(_noopApplication).DefaultTimeout();
+
+ var controlStream = await GetInboundControlStream().DefaultTimeout();
+ await controlStream.ExpectSettingsAsync().DefaultTimeout();
+
+ var outboundControlStream = await CreateControlStream(id: null);
+
+ await AssertIsTrueRetryAsync(
+ () => Connection._streams.Count == 1,
+ "Wait until streams have been created.").DefaultTimeout();
+
+ var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId];
+
+ TriggerTick(now);
+
+ Assert.Equal(TimeSpan.MaxValue.Ticks, serverInboundControlStream.HeaderTimeoutTicks);
+ }
+
+ private static async Task AssertIsTrueRetryAsync(Func assert, string message)
+ {
+ const int Retries = 10;
+
+ for (var i = 0; i < Retries; i++)
+ {
+ if (i > 0)
+ {
+ await Task.Delay((i + 1) * 10);
+ }
+
+ if (assert())
+ {
+ return;
+ }
+ }
+
+ throw new Exception($"Assert failed after {Retries} retries: {message}");
+ }
+ }
+}