diff --git a/Directory.Build.props b/Directory.Build.props index 324cfc4ba367..dc1f3554115a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs index e66d50d2a540..8d6408bb8b37 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs @@ -91,7 +91,8 @@ public Http1OutputProducer( _minResponseDataRateFeature = minResponseDataRateFeature; _outputAborter = outputAborter; - _flusher = new TimingPipeFlusher(_pipeWriter, timeoutControl, log); + _flusher = new TimingPipeFlusher(timeoutControl, log); + _flusher.Initialize(_pipeWriter); } public Task WriteDataAsync(ReadOnlySpan buffer, CancellationToken cancellationToken = default) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index 4c25e6d25097..5822182a288f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -67,7 +67,8 @@ public Http2FrameWriter( _log = serviceContext.Log; _timeoutControl = timeoutControl; _minResponseDataRate = minResponseDataRate; - _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, serviceContext.Log); + _flusher = new TimingPipeFlusher(timeoutControl, serviceContext.Log); + _flusher.Initialize(_outputWriter); _outgoingFrame = new Http2Frame(); _headerEncodingBuffer = new byte[_maxFrameSize]; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index 24b36a8c7c69..6a17db8934e3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -72,7 +72,8 @@ public Http2OutputProducer(Http2Stream stream, Http2StreamContext context, Strea // No need to pass in timeoutControl here, since no minDataRates are passed to the TimingPipeFlusher. // The minimum output data rate is enforced at the connection level by Http2FrameWriter. - _flusher = new TimingPipeFlusher(_pipeWriter, timeoutControl: null, _log); + _flusher = new TimingPipeFlusher(timeoutControl: null, _log); + _flusher.Initialize(_pipeWriter); _dataWriteProcessingTask = ProcessDataWrites(); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs index 1e5bea343771..0dfdb0412634 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2StreamContext.cs @@ -26,7 +26,7 @@ public Http2StreamContext( Http2PeerSettings serverPeerSettings, Http2FrameWriter frameWriter, InputFlowControl connectionInputFlowControl, - OutputFlowControl connectionOutputFlowControl) : base(connectionId, protocols, connectionContext: null!, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint, transport: null!) + OutputFlowControl connectionOutputFlowControl) : base(connectionId, protocols, connectionContext: null!, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint) { StreamId = streamId; StreamLifetimeHandler = streamLifetimeHandler; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 7aca5ce48534..842593acafdc 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -23,6 +23,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3 { internal class Http3Connection : IHttp3StreamLifetimeHandler, IRequestProcessor { + private static readonly object StreamPersistentStateKey = new object(); + // Internal for unit testing internal readonly Dictionary _streams = new Dictionary(); internal IHttp3StreamLifetimeHandler _streamLifetimeHandler; @@ -257,36 +259,37 @@ public async Task ProcessRequestsAsync(IHttpApplication appl Debug.Assert(streamDirectionFeature != null); Debug.Assert(streamIdFeature != null); - var httpConnectionContext = new Http3StreamContext( - streamContext.ConnectionId, - protocols: default, - connectionContext: null!, // TODO connection context is null here. Should we set it to anything? - _context.ServiceContext, - streamContext.Features, - _context.MemoryPool, - streamContext.LocalEndPoint as IPEndPoint, - streamContext.RemoteEndPoint as IPEndPoint, - streamContext.Transport, - _streamLifetimeHandler, - streamContext, - _clientSettings, - _serverSettings); - httpConnectionContext.TimeoutControl = _context.TimeoutControl; - if (!streamDirectionFeature.CanWrite) { // Unidirectional stream - var stream = new Http3ControlStream(application, httpConnectionContext); + var stream = new Http3ControlStream(application, CreateHttpStreamContext(streamContext)); _streamLifetimeHandler.OnStreamCreated(stream); ThreadPool.UnsafeQueueUserWorkItem(stream, preferLocal: false); } else { + var persistentStateFeature = streamContext.Features.Get(); + Debug.Assert(persistentStateFeature != null, $"Required {nameof(IPersistentStateFeature)} not on stream context."); + // Request stream UpdateHighestStreamId(streamIdFeature.StreamId); - var stream = new Http3Stream(application, httpConnectionContext); + Http3Stream stream; + + // Check whether there is an existing HTTP/3 stream on the transport stream. + // A stream will only be cached if the transport stream itself is reused. + if (!persistentStateFeature.State.TryGetValue(StreamPersistentStateKey, out var s)) + { + stream = new Http3Stream(application, CreateHttpStreamContext(streamContext)); + persistentStateFeature.State.Add(StreamPersistentStateKey, stream); + } + else + { + stream = (Http3Stream)s!; + stream.InitializeWithExistingContext(streamContext.Transport); + } + _streamLifetimeHandler.OnStreamCreated(stream); KestrelEventSource.Log.RequestQueuedStart(stream, AspNetCore.Http.HttpProtocol.Http3); @@ -371,6 +374,27 @@ public async Task ProcessRequestsAsync(IHttpApplication appl } } + private Http3StreamContext CreateHttpStreamContext(ConnectionContext streamContext) + { + var httpConnectionContext = new Http3StreamContext( + streamContext.ConnectionId, + protocols: default, + connectionContext: null!, // TODO connection context is null here. Should we set it to anything? + _context.ServiceContext, + streamContext.Features, + _context.MemoryPool, + streamContext.LocalEndPoint as IPEndPoint, + streamContext.RemoteEndPoint as IPEndPoint, + _streamLifetimeHandler, + streamContext, + _clientSettings, + _serverSettings); + httpConnectionContext.TimeoutControl = _context.TimeoutControl; + httpConnectionContext.Transport = streamContext.Transport; + + return httpConnectionContext; + } + private void UpdateConnectionState() { if (_isClosed != 0) @@ -443,12 +467,12 @@ private async ValueTask CreateNewUnidirectionalStreamAsync(application, httpConnectionContext); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index adeafdd6a334..930bb6550231 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -48,16 +48,15 @@ public Http3ControlStream(Http3StreamContext context) _headerType = -1; _frameWriter = new Http3FrameWriter( - context.Transport.Output, context.StreamContext, context.TimeoutControl, httpLimits.MinResponseDataRate, - context.ConnectionId, context.MemoryPool, context.ServiceContext.Log, _streamIdFeature, context.ClientPeerSettings, this); + _frameWriter.Reset(context.Transport.Output, context.ConnectionId); } private void OnStreamClosed() diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs index 894f915c3cdf..495d5ad338f9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3FrameWriter.cs @@ -29,11 +29,9 @@ internal class Http3FrameWriter private readonly object _writeLock = new object(); private readonly int _maxTotalHeaderSize; - private readonly PipeWriter _outputWriter; private readonly ConnectionContext _connectionContext; private readonly ITimeoutControl _timeoutControl; private readonly MinDataRate? _minResponseDataRate; - private readonly string _connectionId; private readonly MemoryPool _memoryPool; private readonly IKestrelTrace _log; private readonly IStreamIdFeature _streamIdFeature; @@ -41,6 +39,9 @@ internal class Http3FrameWriter private readonly Http3RawFrame _outgoingFrame; private readonly TimingPipeFlusher _flusher; + private PipeWriter _outputWriter = default!; + private string _connectionId = default!; + // HTTP/3 doesn't have a max frame size (peer can optionally specify a size). // Write headers to a buffer that can grow. Possible performance improvement // by writing directly to output writer (difficult as frame length is prefixed). @@ -52,19 +53,17 @@ internal class Http3FrameWriter private bool _completed; private bool _aborted; - public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext, ITimeoutControl timeoutControl, MinDataRate? minResponseDataRate, string connectionId, MemoryPool memoryPool, IKestrelTrace log, IStreamIdFeature streamIdFeature, Http3PeerSettings clientPeerSettings, IHttp3Stream http3Stream) + public Http3FrameWriter(ConnectionContext connectionContext, ITimeoutControl timeoutControl, MinDataRate? minResponseDataRate, MemoryPool memoryPool, IKestrelTrace log, IStreamIdFeature streamIdFeature, Http3PeerSettings clientPeerSettings, IHttp3Stream http3Stream) { - _outputWriter = output; _connectionContext = connectionContext; _timeoutControl = timeoutControl; _minResponseDataRate = minResponseDataRate; - _connectionId = connectionId; _memoryPool = memoryPool; _log = log; _streamIdFeature = streamIdFeature; _http3Stream = http3Stream; _outgoingFrame = new Http3RawFrame(); - _flusher = new TimingPipeFlusher(_outputWriter, timeoutControl, log); + _flusher = new TimingPipeFlusher(timeoutControl, log); _headerEncodingBuffer = new ArrayBufferWriter(HeaderBufferSize); // Note that max total header size value doesn't react to settings change during a stream. @@ -76,6 +75,19 @@ public Http3FrameWriter(PipeWriter output, ConnectionContext connectionContext, : (int)clientPeerSettings.MaxRequestHeaderFieldSectionSize; } + public void Reset(PipeWriter output, string connectionId) + { + _outputWriter = output; + _flusher.Initialize(output); + _connectionId = connectionId; + + _headersTotalSize = 0; + _headerEncodingBuffer.Clear(); + _unflushedBytes = 0; + _completed = false; + _aborted = false; + } + internal Task WriteSettingsAsync(List settings) { _outgoingFrame.PrepareSettings(); @@ -238,22 +250,22 @@ internal ValueTask WriteGoAway(long id) private void WriteHeaderUnsynchronized() { _log.Http3FrameSending(_connectionId, _streamIdFeature.StreamId, _outgoingFrame); - var headerLength = WriteHeader(_outgoingFrame, _outputWriter); + var headerLength = WriteHeader(_outgoingFrame.Type, _outgoingFrame.Length, _outputWriter); // We assume the payload will be written prior to the next flush. _unflushedBytes += headerLength + _outgoingFrame.Length; } - internal static int WriteHeader(Http3RawFrame frame, PipeWriter output) + internal static int WriteHeader(Http3FrameType frameType, long frameLength, PipeWriter output) { // max size of the header is 16, most likely it will be smaller. var buffer = output.GetSpan(16); - var typeLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frame.Type); + var typeLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frameType); buffer = buffer.Slice(typeLength); - var lengthLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frame.Length); + var lengthLength = VariableLengthIntegerHelper.WriteInteger(buffer, (int)frameLength); var totalLength = typeLength + lengthLength; output.Advance(typeLength + lengthLength); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs index 724a669bc52b..77f87f60ee93 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs @@ -24,12 +24,13 @@ internal class Http3OutputProducer : IHttpOutputProducer, IHttpOutputAborter private readonly IKestrelTrace _log; private readonly MemoryPool _memoryPool; private readonly Http3Stream _stream; + private readonly Pipe _pipe; private readonly PipeWriter _pipeWriter; private readonly PipeReader _pipeReader; private readonly object _dataWriterLock = new object(); - private readonly ValueTask _dataWriteProcessingTask; + private ValueTask _dataWriteProcessingTask; private bool _startedWritingDataFrames; - private bool _completed; + private bool _streamCompleted; private bool _disposed; private bool _suffixSent; private IMemoryOwner? _fakeMemoryOwner; @@ -45,12 +46,27 @@ public Http3OutputProducer( _stream = stream; _log = log; - var pipe = CreateDataPipe(pool); + _pipe = CreateDataPipe(pool); - _pipeWriter = pipe.Writer; - _pipeReader = pipe.Reader; + _pipeWriter = _pipe.Writer; + _pipeReader = _pipe.Reader; + + _flusher = new TimingPipeFlusher(timeoutControl: null, log); + _flusher.Initialize(_pipeWriter); + _dataWriteProcessingTask = ProcessDataWrites().Preserve(); + } + + public void StreamReset() + { + // Data background task has finished. + Debug.Assert(_dataWriteProcessingTask.IsCompleted); + + _suffixSent = false; + _startedWritingDataFrames = false; + _streamCompleted = false; + + _pipe.Reset(); - _flusher = new TimingPipeFlusher(_pipeWriter, timeoutControl: null, log); _dataWriteProcessingTask = ProcessDataWrites().Preserve(); } @@ -91,7 +107,7 @@ public void Advance(int bytes) { ThrowIfSuffixSent(); - if (_completed) + if (_streamCompleted) { return; } @@ -106,7 +122,7 @@ public void CancelPendingFlush() { lock (_dataWriterLock) { - if (_completed) + if (_streamCompleted) { return; } @@ -141,7 +157,7 @@ public ValueTask FlushAsync(CancellationToken cancellationToken) { ThrowIfSuffixSent(); - if (_completed) + if (_streamCompleted) { return default; } @@ -167,7 +183,7 @@ public Memory GetMemory(int sizeHint = 0) { ThrowIfSuffixSent(); - if (_completed) + if (_streamCompleted) { return GetFakeMemory(sizeHint); } @@ -183,7 +199,7 @@ public Span GetSpan(int sizeHint = 0) { ThrowIfSuffixSent(); - if (_completed) + if (_streamCompleted) { return GetFakeMemory(sizeHint).Span; } @@ -225,12 +241,12 @@ public void Stop() { lock (_dataWriterLock) { - if (_completed) + if (_streamCompleted) { return; } - _completed = true; + _streamCompleted = true; _pipeWriter.Complete(new OperationCanceledException()); } @@ -259,7 +275,7 @@ public Task WriteDataAsync(ReadOnlySpan data, CancellationToken cancellati // This length check is important because we don't want to set _startedWritingDataFrames unless a data // frame will actually be written causing the headers to be flushed. - if (_completed || data.Length == 0) + if (_streamCompleted || data.Length == 0) { return Task.CompletedTask; } @@ -284,7 +300,7 @@ public ValueTask WriteDataToPipeAsync(ReadOnlySpan data, Canc // This length check is important because we don't want to set _startedWritingDataFrames unless a data // frame will actually be written causing the headers to be flushed. - if (_completed || data.Length == 0) + if (_streamCompleted || data.Length == 0) { return default; } @@ -300,7 +316,7 @@ public void WriteResponseHeaders(int statusCode, string? reasonPhrase, HttpRespo { lock (_dataWriterLock) { - if (_completed) + if (_streamCompleted) { return; } @@ -313,12 +329,12 @@ public ValueTask WriteStreamSuffixAsync() { lock (_dataWriterLock) { - if (_completed) + if (_streamCompleted) { return _dataWriteProcessingTask; } - _completed = true; + _streamCompleted = true; _suffixSent = true; _pipeWriter.Complete(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index bfdefac6adcf..344b6afc20e8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -37,18 +37,19 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpH private const PseudoHeaderFields _mandatoryRequestPseudoHeaderFields = PseudoHeaderFields.Method | PseudoHeaderFields.Path | PseudoHeaderFields.Scheme; - private readonly Http3FrameWriter _frameWriter; - private readonly Http3OutputProducer _http3Output; + private Http3FrameWriter _frameWriter = default!; + private Http3OutputProducer _http3Output = default!; + private Http3StreamContext _context = default!; + private IProtocolErrorCodeFeature _errorCodeFeature = default!; + private IStreamIdFeature _streamIdFeature = default!; private int _isClosed; - private readonly Http3StreamContext _context; - private readonly IProtocolErrorCodeFeature _errorCodeFeature; - private readonly IStreamIdFeature _streamIdFeature; private readonly Http3RawFrame _incomingFrame = new Http3RawFrame(); protected RequestHeaderParsingState _requestHeaderParsingState; private PseudoHeaderFields _parsedPseudoHeaderFields; private int _totalParsedHeaderSize; private bool _isMethodConnect; + // TODO: Change to resetable ValueTask source private TaskCompletionSource? _appCompleted; private StreamCompletionFlags _completionState; @@ -58,47 +59,11 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttp3Stream, IHttpH private bool IsAborted => (_completionState & StreamCompletionFlags.Aborted) == StreamCompletionFlags.Aborted; internal bool RstStreamReceived => (_completionState & StreamCompletionFlags.RstStreamReceived) == StreamCompletionFlags.RstStreamReceived; - public Pipe RequestBodyPipe { get; } - - public Http3Stream(Http3StreamContext context) - { - Initialize(context); - - InputRemaining = null; - - _context = context; - - _errorCodeFeature = _context.ConnectionFeatures.Get()!; - _streamIdFeature = _context.ConnectionFeatures.Get()!; - - _frameWriter = new Http3FrameWriter( - context.Transport.Output, - context.StreamContext, - context.TimeoutControl, - context.ServiceContext.ServerOptions.Limits.MinResponseDataRate, - context.ConnectionId, - context.MemoryPool, - context.ServiceContext.Log, - _streamIdFeature, - context.ClientPeerSettings, - this); - - // ResponseHeaders aren't set, kind of ugly that we need to reset. - Reset(); - - _http3Output = new Http3OutputProducer( - _frameWriter, - context.MemoryPool, - this, - context.ServiceContext.Log); - RequestBodyPipe = CreateRequestBodyPipe(64 * 1024); // windowSize? - Output = _http3Output; - QPackDecoder = new QPackDecoder(_context.ServiceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize); - } + public Pipe RequestBodyPipe { get; private set; } = default!; public long? InputRemaining { get; internal set; } - public QPackDecoder QPackDecoder { get; } + public QPackDecoder QPackDecoder { get; private set; } = default!; public PipeReader Input => _context.Transport.Input; @@ -111,6 +76,63 @@ public Http3Stream(Http3StreamContext context) public bool IsRequestStream => true; + public void Initialize(Http3StreamContext context) + { + base.Initialize(context); + + InputRemaining = null; + + _context = context; + + _errorCodeFeature = _context.ConnectionFeatures.Get()!; + _streamIdFeature = _context.ConnectionFeatures.Get()!; + + _appCompleted = null; + _isClosed = 0; + _requestHeaderParsingState = default; + _parsedPseudoHeaderFields = default; + _totalParsedHeaderSize = 0; + _isMethodConnect = false; + _completionState = default; + HeaderTimeoutTicks = 0; + + if (_frameWriter == null) + { + _frameWriter = new Http3FrameWriter( + context.StreamContext, + context.TimeoutControl, + context.ServiceContext.ServerOptions.Limits.MinResponseDataRate, + context.MemoryPool, + context.ServiceContext.Log, + _streamIdFeature, + context.ClientPeerSettings, + this); + + _http3Output = new Http3OutputProducer( + _frameWriter, + context.MemoryPool, + this, + context.ServiceContext.Log); + Output = _http3Output; + RequestBodyPipe = CreateRequestBodyPipe(64 * 1024); // windowSize? + QPackDecoder = new QPackDecoder(_context.ServiceContext.ServerOptions.Limits.Http3.MaxRequestHeaderFieldSize); + } + else + { + _http3Output.StreamReset(); + RequestBodyPipe.Reset(); + QPackDecoder.Reset(); + } + + _frameWriter.Reset(context.Transport.Output, context.ConnectionId); + } + + public void InitializeWithExistingContext(IDuplexPipe transport) + { + _context.Transport = transport; + Initialize(_context); + } + public void Abort(ConnectionAbortedException abortReason, Http3ErrorCode errorCode) { var (oldState, newState) = ApplyCompletionFlag(StreamCompletionFlags.Aborted); @@ -459,9 +481,11 @@ public async Task ProcessRequestAsync(IHttpApplication appli } finally { - await _context.StreamContext.DisposeAsync(); - + // Tells the connection to remove the stream from its active collection. _context.StreamLifetimeHandler.OnStreamCompleted(this); + + // Dispose must happen after stream is no longer active. + await _context.StreamContext.DisposeAsync(); } } } @@ -625,6 +649,9 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) protected override void OnReset() { + _keepAlive = true; + _connectionAborted = false; + // Reset Http3 Features _currentIHttpMinRequestBodyDataRateFeature = this; _currentIHttpResponseTrailersFeature = this; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs index 62ba716326ae..90b44119a35e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3StreamOfT.cs @@ -11,8 +11,9 @@ class Http3Stream : Http3Stream, IHostContextContainer where { private readonly IHttpApplication _application; - public Http3Stream(IHttpApplication application, Http3StreamContext context) : base(context) + public Http3Stream(IHttpApplication application, Http3StreamContext context) { + Initialize(context); _application = application; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs index af595a004a55..f8574f301ada 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3StreamContext.cs @@ -21,11 +21,10 @@ public Http3StreamContext( MemoryPool memoryPool, IPEndPoint? localEndPoint, IPEndPoint? remoteEndPoint, - IDuplexPipe transport, IHttp3StreamLifetimeHandler streamLifetimeHandler, ConnectionContext streamContext, Http3PeerSettings clientPeerSettings, - Http3PeerSettings serverPeerSettings) : base(connectionId, protocols, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint, transport) + Http3PeerSettings serverPeerSettings) : base(connectionId, protocols, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint) { StreamLifetimeHandler = streamLifetimeHandler; StreamContext = streamContext; diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs index 649868531088..acb45e5e291c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs @@ -19,12 +19,10 @@ public HttpConnectionContext( IFeatureCollection connectionFeatures, MemoryPool memoryPool, IPEndPoint? localEndPoint, - IPEndPoint? remoteEndPoint, - IDuplexPipe transport) : base(connectionId, protocols, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint) + IPEndPoint? remoteEndPoint) : base(connectionId, protocols, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint) { - Transport = transport; } - public IDuplexPipe Transport { get; } + public IDuplexPipe Transport { get; set; } = default!; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/TimingPipeFlusher.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/TimingPipeFlusher.cs index 5bab08f56258..0753e3230364 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/TimingPipeFlusher.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/TimingPipeFlusher.cs @@ -19,20 +19,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeW /// internal class TimingPipeFlusher { - private readonly PipeWriter _writer; + private PipeWriter _writer = default!; private readonly ITimeoutControl? _timeoutControl; private readonly IKestrelTrace _log; public TimingPipeFlusher( - PipeWriter writer, ITimeoutControl? timeoutControl, IKestrelTrace log) { - _writer = writer; _timeoutControl = timeoutControl; _log = log; } + public void Initialize(PipeWriter output) + { + _writer = output; + } + public ValueTask FlushAsync() { return FlushAsync(outputAborter: null, cancellationToken: default); diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs index 11c2029aaed1..416f709dfb79 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpConnectionMiddleware.cs @@ -34,8 +34,8 @@ public Task OnConnectionAsync(ConnectionContext connectionContext) connectionContext.Features, memoryPoolFeature?.MemoryPool ?? System.Buffers.MemoryPool.Shared, connectionContext.LocalEndPoint as IPEndPoint, - connectionContext.RemoteEndPoint as IPEndPoint, - connectionContext.Transport); + connectionContext.RemoteEndPoint as IPEndPoint); + httpConnectionContext.Transport = connectionContext.Transport; var connection = new HttpConnection(httpConnectionContext); diff --git a/src/Servers/Kestrel/Core/test/Http3FrameWriterTests.cs b/src/Servers/Kestrel/Core/test/Http3FrameWriterTests.cs index a1ae77ab4bdf..66de1ce795f1 100644 --- a/src/Servers/Kestrel/Core/test/Http3FrameWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/Http3FrameWriterTests.cs @@ -88,7 +88,10 @@ public async Task WriteSettings_TwoSettingsWritten() private Http3FrameWriter CreateFrameWriter(Pipe pipe) { - return new Http3FrameWriter(pipe.Writer, null, null, null, null, _dirtyMemoryPool, null, Mock.Of(), new Http3PeerSettings(), null); + var frameWriter = new Http3FrameWriter(null, null, null, _dirtyMemoryPool, null, Mock.Of(), new Http3PeerSettings(), null); + frameWriter.Reset(pipe.Writer, null); + + return frameWriter; } } } diff --git a/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs index 986f34e2ea12..a29274348136 100644 --- a/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http3HttpProtocolFeatureCollectionTests.cs @@ -20,8 +20,8 @@ public Http3HttpProtocolFeatureCollectionTests() { var streamContext = TestContextFactory.CreateHttp3StreamContext(transport: DuplexPipe.CreateConnectionPair(new PipeOptions(), new PipeOptions()).Application); - var http3Stream = new TestHttp3Stream(streamContext); - http3Stream.Reset(); + var http3Stream = new TestHttp3Stream(); + http3Stream.Initialize(streamContext); _http3Collection = http3Stream; } @@ -59,10 +59,6 @@ public void Http3StreamFeatureCollectionDoesIncludeIHttpMinRequestBodyDataRateFe private class TestHttp3Stream : Http3Stream { - public TestHttp3Stream(Http3StreamContext context) : base(context) - { - } - public override void Execute() { } diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs index 18f20339880d..7b2d229fdb22 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicConnectionContext.cs @@ -94,6 +94,11 @@ public override void Abort(ConnectionAbortedException abortReason) { context = new QuicStreamContext(this, _context); } + else + { + context.ResetFeatureCollection(); + context.ResetItems(); + } context.Initialize(stream); context.Start(); diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs index f9b9c68d7669..339bf89dbce1 100644 --- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs +++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs @@ -106,6 +106,9 @@ public void Initialize(QuicStream stream) Transport = _originalTransport; Application = _originalApplication; + _transportPipeReader.Reset(); + _transportPipeWriter.Reset(); + _connectionId = null; _shutdownReason = null; _streamClosed = false; diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs similarity index 100% rename from src/Servers/Kestrel/perf/Microbenchmarks/Http2ConnectionBenchmarkBase.cs rename to src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2ConnectionEmptyBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs similarity index 100% rename from src/Servers/Kestrel/perf/Microbenchmarks/Http2ConnectionEmptyBenchmark.cs rename to src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionEmptyBenchmark.cs diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2ConnectionHeadersBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionHeadersBenchmark.cs similarity index 100% rename from src/Servers/Kestrel/perf/Microbenchmarks/Http2ConnectionHeadersBenchmark.cs rename to src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionHeadersBenchmark.cs diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2FrameWriterBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs similarity index 100% rename from src/Servers/Kestrel/perf/Microbenchmarks/Http2FrameWriterBenchmark.cs rename to src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2FrameWriterBenchmark.cs diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2HeadersEnumeratorBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2HeadersEnumeratorBenchmark.cs similarity index 100% rename from src/Servers/Kestrel/perf/Microbenchmarks/Http2HeadersEnumeratorBenchmark.cs rename to src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2HeadersEnumeratorBenchmark.cs diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs new file mode 100644 index 000000000000..cf7ed11277e7 --- /dev/null +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionBenchmarkBase.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Net.Http.HPack; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks +{ + public abstract class Http3ConnectionBenchmarkBase + { + private Http3InMemory _http3; + private IHeaderDictionary _httpRequestHeaders; + private Http3RequestHeaderHandler _headerHandler; + private Http3HeadersEnumerator _requestHeadersEnumerator; + private Http3FrameWithPayload _httpFrame; + + protected abstract Task ProcessRequest(HttpContext httpContext); + + private class DefaultTimeoutHandler : ITimeoutHandler + { + public void OnTimeout(TimeoutReason reason) { } + } + + public virtual void GlobalSetup() + { + _headerHandler = new Http3RequestHeaderHandler(); + _requestHeadersEnumerator = new Http3HeadersEnumerator(); + _httpFrame = new Http3FrameWithPayload(); + + _httpRequestHeaders = new HttpRequestHeaders(); + _httpRequestHeaders[HeaderNames.Method] = new StringValues("GET"); + _httpRequestHeaders[HeaderNames.Path] = new StringValues("/"); + _httpRequestHeaders[HeaderNames.Scheme] = new StringValues("http"); + _httpRequestHeaders[HeaderNames.Authority] = new StringValues("localhost:80"); + + var serviceContext = TestContextFactory.CreateServiceContext( + serverOptions: new KestrelServerOptions(), + dateHeaderValueManager: new DateHeaderValueManager(), + systemClock: new MockSystemClock(), + log: new MockTrace()); + serviceContext.DateHeaderValueManager.OnHeartbeat(default); + + var mockSystemClock = new Microsoft.AspNetCore.Testing.MockSystemClock(); + + _http3 = new Http3InMemory(serviceContext, mockSystemClock, new DefaultTimeoutHandler()); + + _http3.InitializeConnectionAsync(ProcessRequest).GetAwaiter().GetResult(); + } + + [Benchmark] + public async Task MakeRequest() + { + _requestHeadersEnumerator.Initialize(_httpRequestHeaders); + + var stream = await _http3.CreateRequestStream(_headerHandler); + + await stream.SendHeadersAsync(_requestHeadersEnumerator); + + while (true) + { + var frame = await stream.ReceiveFrameAsync(allowEnd: true, frame: _httpFrame); + if (frame == null) + { + // Tell stream that is can be reset. + stream.Complete(); + + return; + } + + switch (frame.Type) + { + case System.Net.Http.Http3FrameType.Data: + break; + case System.Net.Http.Http3FrameType.Headers: + break; + default: + throw new InvalidOperationException($"Unexpected frame: {frame.Type}"); + } + } + } + + [GlobalCleanup] + public void Dispose() + { + } + } +} diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionEmptyBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionEmptyBenchmark.cs new file mode 100644 index 000000000000..2d531d64a3d8 --- /dev/null +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http3/Http3ConnectionEmptyBenchmark.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks +{ + public class Http3ConnectionEmptyBenchmark : Http3ConnectionBenchmarkBase + { + [Params(0, 128, 1024)] + public int ResponseDataLength { get; set; } + + private string _responseData; + + [GlobalSetup] + public override void GlobalSetup() + { + base.GlobalSetup(); + _responseData = new string('!', ResponseDataLength); + } + + protected override Task ProcessRequest(HttpContext httpContext) + { + return ResponseDataLength == 0 ? Task.CompletedTask : httpContext.Response.WriteAsync(_responseData); + } + } +} diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj index 3f1b4a98822c..c55afd7e62e3 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks.csproj @@ -10,7 +10,11 @@ - + + + + + diff --git a/src/Servers/Kestrel/shared/CompletionPipeReader.cs b/src/Servers/Kestrel/shared/CompletionPipeReader.cs index b2ae580ce81d..99b1cdbb396a 100644 --- a/src/Servers/Kestrel/shared/CompletionPipeReader.cs +++ b/src/Servers/Kestrel/shared/CompletionPipeReader.cs @@ -58,5 +58,11 @@ public override bool TryRead(out ReadResult result) { return _inner.TryRead(out result); } + + public void Reset() + { + IsCompleted = false; + CompleteException = null; + } } } diff --git a/src/Servers/Kestrel/shared/CompletionPipeWriter.cs b/src/Servers/Kestrel/shared/CompletionPipeWriter.cs index a1ea3615948e..14ee8bfe0b4c 100644 --- a/src/Servers/Kestrel/shared/CompletionPipeWriter.cs +++ b/src/Servers/Kestrel/shared/CompletionPipeWriter.cs @@ -63,5 +63,11 @@ public override Span GetSpan(int sizeHint = 0) { return _inner.GetSpan(sizeHint); } + + public void Reset() + { + IsCompleted = false; + CompleteException = null; + } } } diff --git a/src/Servers/Kestrel/shared/TransportConnection.cs b/src/Servers/Kestrel/shared/TransportConnection.cs index b5c21fad9907..89a905ba9db6 100644 --- a/src/Servers/Kestrel/shared/TransportConnection.cs +++ b/src/Servers/Kestrel/shared/TransportConnection.cs @@ -54,6 +54,11 @@ public override string ConnectionId } } + internal void ResetItems() + { + _items?.Clear(); + } + public override CancellationToken ConnectionClosed { get; set; } // DO NOT remove this override to ConnectionContext.Abort. Doing so would cause diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs new file mode 100644 index 000000000000..73fcfd75a2a6 --- /dev/null +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -0,0 +1,1073 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Net.Http; +using System.Net.Http.QPack; +using System.Text; +using System.Threading.Channels; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.Extensions.Primitives; +using static System.IO.Pipelines.DuplexPipe; +using Http3SettingType = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3SettingType; + +namespace Microsoft.AspNetCore.Testing +{ + internal class Http3InMemory + { + protected static readonly int MaxRequestHeaderFieldSize = 16 * 1024; + protected static readonly string _4kHeaderValue = new string('a', 4096); + protected static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world"); + protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', 16 * 1024)); + + public Http3InMemory(ServiceContext serviceContext, MockSystemClock mockSystemClock, ITimeoutHandler timeoutHandler) + { + _serviceContext = serviceContext; + _timeoutControl = new TimeoutControl(new TimeoutControlConnectionInvoker(this, timeoutHandler)); + _timeoutControl.Debugger = new TestDebugger(); + + _mockSystemClock = mockSystemClock; + + _serverReceivedSettings = Channel.CreateUnbounded>(); + } + + private class TestDebugger : IDebugger + { + public bool IsAttached => false; + } + + private class TimeoutControlConnectionInvoker : ITimeoutHandler + { + private readonly ITimeoutHandler _inner; + private readonly Http3InMemory _http3; + + public TimeoutControlConnectionInvoker(Http3InMemory http3, ITimeoutHandler inner) + { + _http3 = http3; + _inner = inner; + } + + public void OnTimeout(TimeoutReason reason) + { + _inner.OnTimeout(reason); + _http3._httpConnection.OnTimeout(reason); + } + } + + internal ServiceContext _serviceContext; + private MockSystemClock _mockSystemClock; + internal HttpConnection _httpConnection; + internal readonly TimeoutControl _timeoutControl; + internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); + internal readonly ConcurrentQueue _streamContextPool = new ConcurrentQueue(); + protected Task _connectionTask; + + internal readonly ConcurrentDictionary _runningStreams = new ConcurrentDictionary(); + internal readonly Channel> _serverReceivedSettings; + + internal Func OnCreateServerControlStream { get; set; } + private Http3ControlStream _inboundControlStream; + private long _currentStreamId; + internal Http3Connection Connection { get; private set; } + + internal Http3ControlStream OutboundControlStream { get; private set; } + + internal ChannelReader> ServerReceivedSettingsReader => _serverReceivedSettings.Reader; + + internal TestMultiplexedConnectionContext MultiplexedConnectionContext { get; set; } + + internal long GetStreamId(long mask) + { + var id = (_currentStreamId << 2) | mask; + + _currentStreamId += 1; + + return id; + } + + internal async ValueTask GetInboundControlStream() + { + if (_inboundControlStream == null) + { + var reader = MultiplexedConnectionContext.ToClientAcceptQueue.Reader; +#if IS_FUNCTIONAL_TESTS + while (await reader.WaitToReadAsync().DefaultTimeout()) +#else + while (await reader.WaitToReadAsync()) +#endif + { + while (reader.TryRead(out var stream)) + { + _inboundControlStream = stream; + var streamId = await stream.TryReadStreamIdAsync(); + + // -1 means stream was completed. + Debug.Assert(streamId == 0 || streamId == -1, "StreamId sent that was non-zero, which isn't handled by tests"); + + return _inboundControlStream; + } + } + } + + return _inboundControlStream; + } + + internal void CloseConnectionGracefully() + { + MultiplexedConnectionContext.ConnectionClosingCts.Cancel(); + } + + internal Task WaitForConnectionStopAsync(long expectedLastStreamId, bool ignoreNonGoAwayFrames, Http3ErrorCode? expectedErrorCode = null) + { + return WaitForConnectionErrorAsync(ignoreNonGoAwayFrames, expectedLastStreamId, expectedErrorCode: expectedErrorCode ?? 0, matchExpectedErrorMessage: null); + } + + internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long? expectedLastStreamId, Http3ErrorCode expectedErrorCode, Action matchExpectedErrorMessage = null, params string[] expectedErrorMessage) + where TException : Exception + { + var frame = await _inboundControlStream.ReceiveFrameAsync(); + + if (ignoreNonGoAwayFrames) + { + while (frame.Type != Http3FrameType.GoAway) + { + frame = await _inboundControlStream.ReceiveFrameAsync(); + } + } + + if (expectedLastStreamId != null) + { + VerifyGoAway(frame, expectedLastStreamId.GetValueOrDefault()); + } + + AssertConnectionError(expectedErrorCode, matchExpectedErrorMessage, expectedErrorMessage); + + // Verify HttpConnection.ProcessRequestsAsync has exited. +#if IS_FUNCTIONAL_TESTS + await _connectionTask.DefaultTimeout(); +#else + await _connectionTask; +#endif + + // Verify server-to-client control stream has completed. + await _inboundControlStream.ReceiveEndAsync(); + } + + internal void AssertConnectionError(Http3ErrorCode expectedErrorCode, Action matchExpectedErrorMessage = null, params string[] expectedErrorMessage) where TException : Exception + { + var currentError = (Http3ErrorCode)MultiplexedConnectionContext.Error; + if (currentError != expectedErrorCode) + { + throw new InvalidOperationException($"Expected error code {expectedErrorCode}, got {currentError}."); + } + + matchExpectedErrorMessage?.Invoke(typeof(TException), expectedErrorMessage); + } + + internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamId) + { + AssertFrameType(frame.Type, Http3FrameType.GoAway); + var payload = frame.Payload; + if (!VariableLengthIntegerHelper.TryRead(payload.Span, out var streamId, out var _)) + { + throw new InvalidOperationException("Failed to read GO_AWAY stream ID."); + } + if (streamId != expectedLastStreamId) + { + throw new InvalidOperationException($"Expected stream ID {expectedLastStreamId}, got {streamId}."); + } + } + + public void AdvanceClock(TimeSpan timeSpan) + { + var clock = _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); + } + + public void TriggerTick(DateTimeOffset now) + { + _mockSystemClock.UtcNow = now; + Connection?.Tick(now); + } + + public async Task InitializeConnectionAsync(RequestDelegate application) + { + MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this); + + var httpConnectionContext = new HttpMultiplexedConnectionContext( + connectionId: "TestConnectionId", + connectionContext: MultiplexedConnectionContext, + connectionFeatures: MultiplexedConnectionContext.Features, + serviceContext: _serviceContext, + memoryPool: _memoryPool, + localEndPoint: null, + remoteEndPoint: null); + httpConnectionContext.TimeoutControl = _timeoutControl; + + _httpConnection = new HttpConnection(httpConnectionContext); + _httpConnection.Initialize(Connection); + + // ProcessRequestAsync will create the Http3Connection + _connectionTask = _httpConnection.ProcessRequestsAsync(new DummyApplication(application)); + + Connection = (Http3Connection)_httpConnection._requestProcessor; + Connection._streamLifetimeHandler = new LifetimeHandlerInterceptor(Connection, this); + + await GetInboundControlStream(); + } + + public static void AssertFrameType(Http3FrameType actual, Http3FrameType expected) + { + if (actual != expected) + { + throw new InvalidOperationException($"Expected {actual} frame. Got {expected}."); + } + } + + internal async ValueTask InitializeConnectionAndStreamsAsync(RequestDelegate application) + { + await InitializeConnectionAsync(application); + + OutboundControlStream = await CreateControlStream(); + + return await CreateRequestStream(); + } + + private class LifetimeHandlerInterceptor : IHttp3StreamLifetimeHandler + { + private readonly IHttp3StreamLifetimeHandler _inner; + private readonly Http3InMemory _http3TestBase; + + public LifetimeHandlerInterceptor(IHttp3StreamLifetimeHandler inner, Http3InMemory http3TestBase) + { + _inner = inner; + _http3TestBase = http3TestBase; + } + + public bool OnInboundControlStream(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream stream) + { + return _inner.OnInboundControlStream(stream); + } + + public void OnInboundControlStreamSetting(Http3SettingType type, long value) + { + _inner.OnInboundControlStreamSetting(type, value); + + var success = _http3TestBase._serverReceivedSettings.Writer.TryWrite( + new KeyValuePair(type, value)); + Debug.Assert(success); + } + + public bool OnInboundDecoderStream(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream stream) + { + return _inner.OnInboundDecoderStream(stream); + } + + public bool OnInboundEncoderStream(Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3ControlStream stream) + { + return _inner.OnInboundEncoderStream(stream); + } + + public void OnStreamCompleted(IHttp3Stream stream) + { + _inner.OnStreamCompleted(stream); + + if (_http3TestBase._runningStreams.TryRemove(stream.StreamId, out var testStream)) + { + testStream._onStreamCompletedTcs.TrySetResult(); + } + } + + public void OnStreamConnectionError(Http3ConnectionErrorException ex) + { + _inner.OnStreamConnectionError(ex); + } + + public void OnStreamCreated(IHttp3Stream stream) + { + _inner.OnStreamCreated(stream); + + if (_http3TestBase._runningStreams.TryGetValue(stream.StreamId, out var testStream)) + { + testStream._onStreamCreatedTcs.TrySetResult(); + } + } + + public void OnStreamHeaderReceived(IHttp3Stream stream) + { + _inner.OnStreamHeaderReceived(stream); + + if (_http3TestBase._runningStreams.TryGetValue(stream.StreamId, out var testStream)) + { + testStream._onHeaderReceivedTcs.TrySetResult(); + } + } + } + + protected void ConnectionClosed() + { + + } + + public static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler writerScheduler) => new PipeOptions + ( + pool: memoryPool, + readerScheduler: serviceContext.Scheduler, + writerScheduler: writerScheduler, + pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, + resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, + useSynchronizationContext: false, + minimumSegmentSize: memoryPool.GetMinimumSegmentSize() + ); + + public static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler readerScheduler) => new PipeOptions + ( + pool: memoryPool, + readerScheduler: readerScheduler, + writerScheduler: serviceContext.Scheduler, + pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext), + resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext), + useSynchronizationContext: false, + minimumSegmentSize: memoryPool.GetMinimumSegmentSize() + ); + + private static long GetOutputResponseBufferSize(ServiceContext serviceContext) + { + var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize; + if (bufferSize == 0) + { + // 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly + return 1; + } + + // null means that we have no back pressure + return bufferSize ?? 0; + } + + internal ValueTask CreateControlStream() + { + return CreateControlStream(id: 0); + } + + internal async ValueTask CreateControlStream(int? id) + { + var testStreamContext = new TestStreamContext(canRead: true, canWrite: false, this); + testStreamContext.Initialize(GetStreamId(0x02)); + + var stream = new Http3ControlStream(this, testStreamContext); + _runningStreams[stream.StreamId] = stream; + + MultiplexedConnectionContext.ToServerAcceptQueue.Writer.TryWrite(stream.StreamContext); + if (id != null) + { + await stream.WriteStreamIdAsync(id.GetValueOrDefault()); + } + return stream; + } + + internal ValueTask CreateRequestStream(Http3RequestHeaderHandler headerHandler = null) + { + if (!_streamContextPool.TryDequeue(out var testStreamContext)) + { + testStreamContext = new TestStreamContext(canRead: true, canWrite: true, this); + } + testStreamContext.Initialize(GetStreamId(0x00)); + + var stream = new Http3RequestStream(this, Connection, testStreamContext, headerHandler ?? new Http3RequestHeaderHandler()); + _runningStreams[stream.StreamId] = stream; + + MultiplexedConnectionContext.ToServerAcceptQueue.Writer.TryWrite(stream.StreamContext); + return new ValueTask(stream); + } + } + + internal class Http3StreamBase : IProtocolErrorCodeFeature + { + internal TaskCompletionSource _onStreamCreatedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + internal TaskCompletionSource _onStreamCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + internal TaskCompletionSource _onHeaderReceivedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + internal ConnectionContext StreamContext { get; } + internal IProtocolErrorCodeFeature _protocolErrorCodeFeature; + internal DuplexPipe.DuplexPipePair _pair; + internal Http3InMemory _testBase; + internal Http3Connection _connection; + public long BytesReceived { get; private set; } + public long Error + { + get => _protocolErrorCodeFeature.Error; + set => _protocolErrorCodeFeature.Error = value; + } + + public Task OnStreamCreatedTask => _onStreamCreatedTcs.Task; + public Task OnStreamCompletedTask => _onStreamCompletedTcs.Task; + public Task OnHeaderReceivedTask => _onHeaderReceivedTcs.Task; + + public Http3StreamBase(TestStreamContext testStreamContext) + { + StreamContext = testStreamContext; + _protocolErrorCodeFeature = testStreamContext; + _pair = testStreamContext._pair; + } + + protected Task SendAsync(ReadOnlySpan span) + { + var writableBuffer = _pair.Application.Output; + writableBuffer.Write(span); + return FlushAsync(writableBuffer); + } + + protected static Task FlushAsync(PipeWriter writableBuffer) + { + var task = writableBuffer.FlushAsync(); +#if IS_FUNCTIONAL_TESTS + return task.AsTask().DefaultTimeout(); +#else + return task.GetAsTask(); +#endif + } + + internal async Task ReceiveEndAsync() + { + var result = await ReadApplicationInputAsync(); + if (!result.IsCompleted) + { + throw new InvalidOperationException("End not received."); + } + } + +#if IS_FUNCTIONAL_TESTS + protected Task ReadApplicationInputAsync() + { + return _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); + } +#else + protected ValueTask ReadApplicationInputAsync() + { + return _pair.Application.Input.ReadAsync(); + } +#endif + + internal async ValueTask ReceiveFrameAsync(bool expectEnd = false, bool allowEnd = false, Http3FrameWithPayload frame = null) + { + frame ??= new Http3FrameWithPayload(); + + while (true) + { + var result = await ReadApplicationInputAsync(); + var buffer = result.Buffer; + var consumed = buffer.Start; + var examined = buffer.Start; + var copyBuffer = buffer; + + try + { + if (buffer.Length == 0) + { + if (result.IsCompleted && allowEnd) + { + return null; + } + + throw new InvalidOperationException("No data received."); + } + + if (Http3FrameReader.TryReadFrame(ref buffer, frame, out var framePayload)) + { + consumed = examined = framePayload.End; + frame.Payload = framePayload.ToArray(); + + if (expectEnd) + { + if (!result.IsCompleted || buffer.Length > 0) + { + throw new Exception("Reader didn't complete with frame"); + } + } + + return frame; + } + else + { + examined = buffer.End; + } + + if (result.IsCompleted) + { + throw new IOException("The reader completed without returning a frame."); + } + } + finally + { + BytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length; + _pair.Application.Input.AdvanceTo(consumed, examined); + } + } + } + + internal async Task SendFrameAsync(Http3FrameType frameType, Memory data, bool endStream = false) + { + var outputWriter = _pair.Application.Output; + Http3FrameWriter.WriteHeader(frameType, data.Length, outputWriter); + + if (!endStream) + { + await SendAsync(data.Span); + } + else + { + // Write and end stream at the same time. + // Avoid race condition of frame read separately from end of stream. + await EndStreamAsync(data.Span); + } + } + + internal Task EndStreamAsync(ReadOnlySpan span = default) + { + var writableBuffer = _pair.Application.Output; + if (span.Length > 0) + { + writableBuffer.Write(span); + } + return writableBuffer.CompleteAsync().AsTask(); + } + + internal async Task WaitForStreamErrorAsync(Http3ErrorCode protocolError, Action matchExpectedErrorMessage = null, string expectedErrorMessage = null) + { + var result = await ReadApplicationInputAsync(); + if (!result.IsCompleted) + { + throw new InvalidOperationException("Stream not ended."); + } + if ((Http3ErrorCode)Error != protocolError) + { + throw new InvalidOperationException($"Expected error code {protocolError}, got {(Http3ErrorCode)Error}."); + } + + matchExpectedErrorMessage?.Invoke(expectedErrorMessage); + } + } + + internal class Http3RequestHeaderHandler + { + public readonly byte[] HeaderEncodingBuffer = new byte[64 * 1024]; + public readonly QPackDecoder QpackDecoder = new QPackDecoder(8192); + public readonly Dictionary DecodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler + { + private readonly TestStreamContext _testStreamContext; + private readonly Http3RequestHeaderHandler _headerHandler; + private readonly long _streamId; + + public bool CanRead => true; + public bool CanWrite => true; + + public long StreamId => _streamId; + + public bool Disposed => _testStreamContext.Disposed; + public Task OnDisposedTask => _testStreamContext.OnDisposedTask; + + public Http3RequestStream(Http3InMemory testBase, Http3Connection connection, TestStreamContext testStreamContext, Http3RequestHeaderHandler headerHandler) + : base(testStreamContext) + { + _testBase = testBase; + _connection = connection; + _streamId = testStreamContext.StreamId; + _testStreamContext = testStreamContext; + this._headerHandler = headerHandler; + } + + public Task SendHeadersAsync(IEnumerable> headers, bool endStream = false) + { + return SendHeadersAsync(GetHeadersEnumerator(headers), endStream); + } + + public async Task SendHeadersAsync(Http3HeadersEnumerator headers, bool endStream = false) + { + var headersTotalSize = 0; + + var buffer = _headerHandler.HeaderEncodingBuffer.AsMemory(); + var done = QPackHeaderWriter.BeginEncode(headers, buffer.Span, ref headersTotalSize, out var length); + if (!done) + { + throw new InvalidOperationException("Headers not sent."); + } + + await SendFrameAsync(Http3FrameType.Headers, buffer.Slice(0, length), endStream); + } + + internal Http3HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) + { + var dictionary = headers + .GroupBy(g => g.Key) + .ToDictionary(g => g.Key, g => new StringValues(g.Select(values => values.Value).ToArray())); + + var headersEnumerator = new Http3HeadersEnumerator(); + headersEnumerator.Initialize(dictionary); + return headersEnumerator; + } + + internal async Task SendHeadersPartialAsync() + { + // Send HEADERS frame header without content. + var outputWriter = _pair.Application.Output; + Http3FrameWriter.WriteHeader(Http3FrameType.Data, frameLength: 10, outputWriter); + await SendAsync(Span.Empty); + } + + internal async Task SendDataAsync(Memory data, bool endStream = false) + { + await SendFrameAsync(Http3FrameType.Data, data, endStream); + } + + internal async ValueTask> ExpectHeadersAsync(bool expectEnd = false) + { + var http3WithPayload = await ReceiveFrameAsync(expectEnd); + Http3InMemory.AssertFrameType(http3WithPayload.Type, Http3FrameType.Headers); + + _headerHandler.DecodedHeaders.Clear(); + _headerHandler.QpackDecoder.Decode(http3WithPayload.PayloadSequence, this); + _headerHandler.QpackDecoder.Reset(); + return _headerHandler.DecodedHeaders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, _headerHandler.DecodedHeaders.Comparer); + } + + internal async ValueTask> ExpectDataAsync() + { + var http3WithPayload = await ReceiveFrameAsync(); + return http3WithPayload.Payload; + } + + internal async Task ExpectReceiveEndOfStream() + { + var result = await ReadApplicationInputAsync(); + if (!result.IsCompleted) + { + throw new InvalidOperationException("End of stream not received."); + } + } + + public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) + { + _headerHandler.DecodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); + } + + public void OnHeadersComplete(bool endHeaders) + { + } + + public void OnStaticIndexedHeader(int index) + { + var knownHeader = H3StaticTable.GetHeaderFieldAt(index); + _headerHandler.DecodedHeaders[((Span)knownHeader.Name).GetAsciiStringNonNullCharacters()] = HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan)knownHeader.Value); + } + + public void OnStaticIndexedHeader(int index, ReadOnlySpan value) + { + _headerHandler.DecodedHeaders[((Span)H3StaticTable.GetHeaderFieldAt(index).Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); + } + + public void Complete() + { + _testStreamContext.Complete(); + } + } + + internal class Http3FrameWithPayload : Http3RawFrame + { + public Http3FrameWithPayload() : base() + { + } + + // This does not contain extended headers + public Memory Payload { get; set; } + + public ReadOnlySequence PayloadSequence => new ReadOnlySequence(Payload); + } + + public enum StreamInitiator + { + Client, + Server + } + + internal class Http3ControlStream : Http3StreamBase + { + private readonly long _streamId; + + public bool CanRead => true; + public bool CanWrite => false; + + public long StreamId => _streamId; + + public Http3ControlStream(Http3InMemory testBase, TestStreamContext testStreamContext) + : base(testStreamContext) + { + _testBase = testBase; + _streamId = testStreamContext.StreamId; + } + + internal async ValueTask> ExpectSettingsAsync() + { + var http3WithPayload = await ReceiveFrameAsync(); + Http3InMemory.AssertFrameType(http3WithPayload.Type, Http3FrameType.Settings); + + var payload = http3WithPayload.PayloadSequence; + + var settings = new Dictionary(); + while (true) + { + var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _); + if (id == -1) + { + break; + } + + payload = payload.Slice(consumed); + + var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _); + if (value == -1) + { + break; + } + + payload = payload.Slice(consumed); + settings.Add(id, value); + } + + return settings; + } + + public async Task WriteStreamIdAsync(int id) + { + var writableBuffer = _pair.Application.Output; + + void WriteSpan(PipeWriter pw) + { + var buffer = pw.GetSpan(sizeHint: 8); + var lengthWritten = VariableLengthIntegerHelper.WriteInteger(buffer, id); + pw.Advance(lengthWritten); + } + + WriteSpan(writableBuffer); + + await FlushAsync(writableBuffer); + } + + internal async Task SendGoAwayAsync(long streamId, bool endStream = false) + { + var data = new byte[VariableLengthIntegerHelper.GetByteCount(streamId)]; + VariableLengthIntegerHelper.WriteInteger(data, streamId); + + await SendFrameAsync(Http3FrameType.GoAway, data, endStream); + } + + internal async Task SendSettingsAsync(List settings, bool endStream = false) + { + var settingsLength = CalculateSettingsSize(settings); + var buffer = new byte[settingsLength]; + WriteSettings(settings, buffer); + + await SendFrameAsync(Http3FrameType.Settings, buffer, endStream); + } + + internal static int CalculateSettingsSize(List settings) + { + var length = 0; + foreach (var setting in settings) + { + length += VariableLengthIntegerHelper.GetByteCount((long)setting.Parameter); + length += VariableLengthIntegerHelper.GetByteCount(setting.Value); + } + return length; + } + + internal static void WriteSettings(List settings, Span destination) + { + foreach (var setting in settings) + { + var parameterLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Parameter); + destination = destination.Slice(parameterLength); + + var valueLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Value); + destination = destination.Slice(valueLength); + } + } + + public async ValueTask TryReadStreamIdAsync() + { + while (true) + { + var result = await ReadApplicationInputAsync(); + var readableBuffer = result.Buffer; + var consumed = readableBuffer.Start; + var examined = readableBuffer.End; + + try + { + if (!readableBuffer.IsEmpty) + { + var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined); + if (id != -1) + { + return id; + } + } + + if (result.IsCompleted) + { + return -1; + } + } + finally + { + _pair.Application.Input.AdvanceTo(consumed, examined); + } + } + } + } + + internal class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, IProtocolErrorCodeFeature + { + public readonly Channel ToServerAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + public readonly Channel ToClientAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + private readonly Http3InMemory _testBase; + private long _error; + + public TestMultiplexedConnectionContext(Http3InMemory testBase) + { + _testBase = testBase; + Features = new FeatureCollection(); + Features.Set(this); + Features.Set(this); + Features.Set(this); + ConnectionClosedRequested = ConnectionClosingCts.Token; + } + + public override string ConnectionId { get; set; } + + public override IFeatureCollection Features { get; } + + public override IDictionary Items { get; set; } + + public CancellationToken ConnectionClosedRequested { get; set; } + + public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource(); + + public long Error + { + get => _error; + set => _error = value; + } + + public override void Abort() + { + Abort(new ConnectionAbortedException()); + } + + public override void Abort(ConnectionAbortedException abortReason) + { + ToServerAcceptQueue.Writer.TryComplete(); + ToClientAcceptQueue.Writer.TryComplete(); + } + + public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + while (await ToServerAcceptQueue.Reader.WaitToReadAsync()) + { + while (ToServerAcceptQueue.Reader.TryRead(out var connection)) + { + return connection; + } + } + + return null; + } + + public override ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default) + { + var testStreamContext = new TestStreamContext(canRead: true, canWrite: false, _testBase); + testStreamContext.Initialize(_testBase.GetStreamId(0x03)); + + var stream = _testBase.OnCreateServerControlStream?.Invoke(testStreamContext) ?? new Http3ControlStream(_testBase, testStreamContext); + ToClientAcceptQueue.Writer.WriteAsync(stream); + return new ValueTask(stream.StreamContext); + } + + public void OnHeartbeat(Action action, object state) + { + } + + public void RequestClose() + { + throw new NotImplementedException(); + } + } + + internal class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature, IProtocolErrorCodeFeature, IPersistentStateFeature + { + private readonly Http3InMemory _testBase; + + internal DuplexPipePair _pair; + private Pipe _inputPipe; + private Pipe _outputPipe; + private CompletionPipeReader _transportPipeReader; + private CompletionPipeWriter _transportPipeWriter; + + private bool _isAborted; + private bool _isComplete; + + // Persistent state collection is not reset with a stream by design. + private IDictionary _persistentState; + + private TaskCompletionSource _disposedTcs; + + public TestStreamContext(bool canRead, bool canWrite, Http3InMemory testBase) + { + Features = new FeatureCollection(); + CanRead = canRead; + CanWrite = canWrite; + _testBase = testBase; + } + + public void Initialize(long streamId) + { + if (!_isComplete) + { + // Create new pipes when test stream context is reused rather than reseting them. + // This is required because the client tests read from these directly from these pipes. + // When a request is finished they'll check to see whether there is anymore content + // in the Application.Output pipe. If it has been reset then that code will error. + var inputOptions = Http3InMemory.GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); + var outputOptions = Http3InMemory.GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); + + _inputPipe = new Pipe(inputOptions); + _outputPipe = new Pipe(outputOptions); + + _transportPipeReader = new CompletionPipeReader(_inputPipe.Reader); + _transportPipeWriter = new CompletionPipeWriter(_outputPipe.Writer); + + _pair = new DuplexPipePair( + new DuplexPipe(_transportPipeReader, _transportPipeWriter), + new DuplexPipe(_outputPipe.Reader, _inputPipe.Writer)); + } + else + { + _pair.Application.Input.Complete(); + _pair.Application.Output.Complete(); + + _transportPipeReader.Reset(); + _transportPipeWriter.Reset(); + + _inputPipe.Reset(); + _outputPipe.Reset(); + } + + Features.Set(this); + Features.Set(this); + Features.Set(this); + Features.Set(this); + + StreamId = streamId; + + _disposedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Disposed = false; + } + + public bool Disposed { get; private set; } + + public Task OnDisposedTask => _disposedTcs.Task; + + public override string ConnectionId { get; set; } + + public long StreamId { get; private set; } + + public override IFeatureCollection Features { get; } + + public override IDictionary Items { get; set; } + + public override IDuplexPipe Transport + { + get + { + return _pair.Transport; + } + set + { + throw new NotImplementedException(); + } + } + + public bool CanRead { get; } + + public bool CanWrite { get; } + + public long Error { get; set; } + + public override void Abort(ConnectionAbortedException abortReason) + { + _isAborted = true; + _pair.Application.Output.Complete(abortReason); + } + + public override ValueTask DisposeAsync() + { + Disposed = true; + _disposedTcs.TrySetResult(); + + if (!_isAborted && + _transportPipeReader.IsCompletedSuccessfully && + _transportPipeWriter.IsCompletedSuccessfully) + { + _testBase._streamContextPool.Enqueue(this); + } + + return ValueTask.CompletedTask; + } + + internal void Complete() + { + _isComplete = true; + } + + IDictionary IPersistentStateFeature.State + { + get + { + // Lazily allocate persistent state + return _persistentState ?? (_persistentState = new ConnectionItems()); + } + } + } +} diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 77f79afb2160..f6c13ff6eeb4 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -66,9 +66,9 @@ public static HttpConnectionContext CreateHttpConnectionContext( connectionFeatures, memoryPool ?? MemoryPool.Shared, localEndPoint, - remoteEndPoint, - transport); + remoteEndPoint); context.TimeoutControl = timeoutControl; + context.Transport = transport; return context; } @@ -185,13 +185,13 @@ public static Http3StreamContext CreateHttp3StreamContext( memoryPool: memoryPool ?? MemoryPool.Shared, localEndPoint: localEndPoint, remoteEndPoint: remoteEndPoint, - transport: transport, streamLifetimeHandler: streamLifetimeHandler, streamContext: null, clientPeerSettings: new Http3PeerSettings(), serverPeerSettings: null ); context.TimeoutControl = timeoutControl; + context.Transport = transport; return context; } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 90e10944b5b9..fa7a0bb03346 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Net.Http.Headers; using Xunit; +using Http3SettingType = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3SettingType; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { @@ -21,7 +22,7 @@ public class Http3ConnectionTests : Http3TestBase public async Task CreateRequestStream_RequestCompleted_Disposed() { var appCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - await InitializeConnectionAsync(async context => + await Http3Api.InitializeConnectionAsync(async context => { var buffer = new byte[16 * 1024]; var received = 0; @@ -34,10 +35,10 @@ await InitializeConnectionAsync(async context => await appCompletedTcs.Task; }); - await CreateControlStream(); - await GetInboundControlStream(); + await Http3Api.CreateControlStream(); + await Http3Api.GetInboundControlStream(); - var requestStream = await CreateRequestStream(); + var requestStream = await Http3Api.CreateRequestStream(); var headers = new[] { @@ -57,23 +58,24 @@ await InitializeConnectionAsync(async context => var responseData = await requestStream.ExpectDataAsync(); Assert.Equal("Hello world", Encoding.ASCII.GetString(responseData.ToArray())); + await requestStream.OnDisposedTask.DefaultTimeout(); Assert.True(requestStream.Disposed); } [Fact] public async Task GracefulServerShutdownClosesConnection() { - await InitializeConnectionAsync(_echoApplication); + await Http3Api.InitializeConnectionAsync(_echoApplication); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); // Trigger server shutdown. - CloseConnectionGracefully(); + Http3Api.CloseConnectionGracefully(); - Assert.Null(await MultiplexedConnectionContext.AcceptAsync().DefaultTimeout()); + Assert.Null(await Http3Api.MultiplexedConnectionContext.AcceptAsync().DefaultTimeout()); - await WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); + await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); } [Theory] @@ -84,17 +86,17 @@ public async Task GracefulServerShutdownClosesConnection() [InlineData(0x5)] public async Task SETTINGS_ReservedSettingSent_ConnectionError(long settingIdentifier) { - await InitializeConnectionAsync(_echoApplication); + await Http3Api.InitializeConnectionAsync(_echoApplication); - var outboundcontrolStream = await CreateControlStream(); + var outboundcontrolStream = await Http3Api.CreateControlStream(); await outboundcontrolStream.SendSettingsAsync(new List { - new Http3PeerSetting((Internal.Http3.Http3SettingType) settingIdentifier, 0) // reserved value + new Http3PeerSetting((Http3SettingType) settingIdentifier, 0) // reserved value }); - await GetInboundControlStream(); + await Http3Api.GetInboundControlStream(); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.SettingsError, @@ -107,12 +109,12 @@ await WaitForConnectionErrorAsync( [InlineData(3, "decoder")] public async Task InboundStreams_CreateMultiple_ConnectionError(int streamId, string name) { - await InitializeConnectionAsync(_noopApplication); + await Http3Api.InitializeConnectionAsync(_noopApplication); - await CreateControlStream(streamId); - await CreateControlStream(streamId); + await Http3Api.CreateControlStream(streamId); + await Http3Api.CreateControlStream(streamId); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.StreamCreationError, @@ -125,32 +127,31 @@ await WaitForConnectionErrorAsync( [InlineData(nameof(Http3FrameType.PushPromise))] public async Task ControlStream_ClientToServer_UnexpectedFrameType_ConnectionError(string frameType) { - await InitializeConnectionAsync(_noopApplication); + await Http3Api.InitializeConnectionAsync(_noopApplication); - var controlStream = await CreateControlStream(); + var controlStream = await Http3Api.CreateControlStream(); - var frame = new Http3RawFrame(); - frame.Type = Enum.Parse(frameType); - await controlStream.SendFrameAsync(frame, Memory.Empty); + var f = Enum.Parse(frameType); + await controlStream.SendFrameAsync(f, Memory.Empty); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.UnexpectedFrame, - expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(frame.Type))); + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(f))); } [Fact] public async Task ControlStream_ClientToServer_ClientCloses_ConnectionError() { - await InitializeConnectionAsync(_noopApplication); + await Http3Api.InitializeConnectionAsync(_noopApplication); - var controlStream = await CreateControlStream(id: 0); + var controlStream = await Http3Api.CreateControlStream(id: 0); await controlStream.SendSettingsAsync(new List()); await controlStream.EndStreamAsync(); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 0, expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, @@ -160,9 +161,9 @@ await WaitForConnectionErrorAsync( [Fact] public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError() { - OnCreateServerControlStream = () => + Http3Api.OnCreateServerControlStream = testStreamContext => { - var controlStream = new Http3ControlStream(this, StreamInitiator.Server); + var controlStream = new Microsoft.AspNetCore.Testing.Http3ControlStream(Http3Api, testStreamContext); // Make server connection error when trying to write to control stream. controlStream.StreamContext.Transport.Output.Complete(); @@ -170,9 +171,9 @@ public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError return controlStream; }; - await InitializeConnectionAsync(_noopApplication); + await Http3Api.InitializeConnectionAsync(_noopApplication); - AssertConnectionError( + Http3Api.AssertConnectionError( expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, expectedErrorMessage: CoreStrings.Http3ControlStreamErrorInitializingOutbound); } @@ -180,29 +181,83 @@ public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError [Fact] public async Task SETTINGS_MaxFieldSectionSizeSent_ServerReceivesValue() { - await InitializeConnectionAsync(_echoApplication); + await Http3Api.InitializeConnectionAsync(_echoApplication); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); var incomingSettings = await inboundControlStream.ExpectSettingsAsync(); var defaultLimits = new KestrelServerLimits(); Assert.Collection(incomingSettings, kvp => { - Assert.Equal((long)Internal.Http3.Http3SettingType.MaxFieldSectionSize, kvp.Key); + Assert.Equal((long)Http3SettingType.MaxFieldSectionSize, kvp.Key); Assert.Equal(defaultLimits.MaxRequestHeadersTotalSize, kvp.Value); }); - var outboundcontrolStream = await CreateControlStream(); + var outboundcontrolStream = await Http3Api.CreateControlStream(); await outboundcontrolStream.SendSettingsAsync(new List { - new Http3PeerSetting(Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100) + new Http3PeerSetting(Http3SettingType.MaxFieldSectionSize, 100) }); - var maxFieldSetting = await ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); + var maxFieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); - Assert.Equal(Internal.Http3.Http3SettingType.MaxFieldSectionSize, maxFieldSetting.Key); + Assert.Equal(Http3SettingType.MaxFieldSectionSize, maxFieldSetting.Key); Assert.Equal(100, maxFieldSetting.Value); } + + [Fact] + public async Task StreamPool_MultipleStreamsInSequence_PooledStreamReused() + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await Http3Api.InitializeConnectionAsync(_echoApplication); + + var requestStream = await Http3Api.CreateRequestStream(); + var streamContext1 = requestStream.StreamContext; + + await requestStream.SendHeadersAsync(headers); + await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world 1"), endStream: true); + + Assert.False(requestStream.Disposed); + + await requestStream.ExpectHeadersAsync(); + var responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello world 1", Encoding.ASCII.GetString(responseData.ToArray())); + + await requestStream.ExpectReceiveEndOfStream(); + + await requestStream.OnStreamCompletedTask.DefaultTimeout(); + + await requestStream.OnDisposedTask.DefaultTimeout(); + Assert.True(requestStream.Disposed); + + requestStream = await Http3Api.CreateRequestStream(); + var streamContext2 = requestStream.StreamContext; + + await requestStream.SendHeadersAsync(headers); + await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world 2"), endStream: true); + + Assert.False(requestStream.Disposed); + + await requestStream.ExpectHeadersAsync(); + responseData = await requestStream.ExpectDataAsync(); + Assert.Equal("Hello world 2", Encoding.ASCII.GetString(responseData.ToArray())); + + await requestStream.ExpectReceiveEndOfStream(); + + await requestStream.OnStreamCompletedTask.DefaultTimeout(); + + await requestStream.OnDisposedTask.DefaultTimeout(); + Assert.True(requestStream.Disposed); + + Assert.Same(streamContext1, streamContext2); + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index eb16f5df6983..44925b6b6ac1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -35,7 +35,7 @@ public async Task HelloWorldTest() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); await requestStream.SendHeadersAsync(headers); await requestStream.SendDataAsync(Encoding.ASCII.GetBytes("Hello world"), endStream: true); @@ -58,7 +58,7 @@ public async Task UnauthorizedHttpStatusResponse() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { context.Response.StatusCode = 401; return Task.CompletedTask; @@ -83,9 +83,12 @@ public async Task EmptyMethod_Reset() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); await requestStream.SendHeadersAsync(headers); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3ErrorMethodInvalid("")); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, + CoreStrings.FormatHttp3ErrorMethodInvalid("")); } [Fact] @@ -99,9 +102,12 @@ public async Task InvalidCustomMethod_Reset() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); await requestStream.SendHeadersAsync(headers); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3ErrorMethodInvalid("Hello,World")); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, + CoreStrings.FormatHttp3ErrorMethodInvalid("Hello,World")); } [Fact] @@ -115,7 +121,7 @@ public async Task CustomMethod_Accepted() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoMethod); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoMethod); await requestStream.SendHeadersAsync(headers, endStream: true); var responseHeaders = await requestStream.ExpectHeadersAsync(); @@ -139,19 +145,20 @@ public async Task RequestHeadersMaxRequestHeaderFieldSize_EndsStream() new KeyValuePair("test", new string('a', 20000)) }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); await requestStream.SendHeadersAsync(headers); await requestStream.WaitForStreamErrorAsync( Http3ErrorCode.InternalError, + AssertExpectedErrorMessages, "The HTTP headers length exceeded the set limit of 16384 bytes."); } [Fact] public async Task ConnectMethod_Accepted() { - var requestStream = await InitializeConnectionAndStreamsAsync(_echoMethod); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoMethod); // :path and :scheme are not allowed, :authority is optional var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT") }; @@ -170,7 +177,7 @@ public async Task ConnectMethod_Accepted() [Fact] public async Task OptionsStar_LeftOutOfPath() { - var requestStream = await InitializeConnectionAndStreamsAsync(_echoPath); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoPath); var headers = new[] { new KeyValuePair(HeaderNames.Method, "OPTIONS"), new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.Path, "*")}; @@ -190,7 +197,7 @@ public async Task OptionsStar_LeftOutOfPath() [Fact] public async Task OptionsSlash_Accepted() { - var requestStream = await InitializeConnectionAndStreamsAsync(_echoPath); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoPath); var headers = new[] { new KeyValuePair(HeaderNames.Method, "OPTIONS"), new KeyValuePair(HeaderNames.Scheme, "http"), @@ -211,7 +218,7 @@ public async Task OptionsSlash_Accepted() [Fact] public async Task PathAndQuery_Separated() { - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { context.Response.Headers["path"] = context.Request.Path.Value; context.Response.Headers["query"] = context.Request.QueryString.Value; @@ -248,7 +255,7 @@ public async Task PathAndQuery_Separated() [InlineData("/a/b/c/.%2E/d", "/a/b/d")] // Decode before navigation processing public async Task Path_DecodedAndNormalized(string input, string expected) { - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { Assert.Equal(expected, context.Request.Path.Value); Assert.Equal(input, context.Features.Get().RawTarget); @@ -275,7 +282,7 @@ public async Task Path_DecodedAndNormalized(string input, string expected) [InlineData(":scheme", "http")] public async Task ConnectMethod_WithSchemeOrPath_Reset(string headerName, string value) { - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); // :path and :scheme are not allowed, :authority is optional var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT"), @@ -283,7 +290,10 @@ public async Task ConnectMethod_WithSchemeOrPath_Reset(string headerName, string await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.Http3ErrorConnectMustNotSendSchemeOrPath); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, + CoreStrings.Http3ErrorConnectMustNotSendSchemeOrPath); } [Theory] @@ -291,7 +301,7 @@ public async Task ConnectMethod_WithSchemeOrPath_Reset(string headerName, string [InlineData("ftp")] public async Task SchemeMismatch_Reset(string scheme) { - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"), new KeyValuePair(HeaderNames.Path, "/"), @@ -299,7 +309,10 @@ public async Task SchemeMismatch_Reset(string scheme) await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3StreamErrorSchemeMismatch(scheme, "http")); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, + CoreStrings.FormatHttp3StreamErrorSchemeMismatch(scheme, "http")); } [Theory] @@ -309,7 +322,7 @@ public async Task SchemeMismatchAllowed_Processed(string scheme) { _serviceContext.ServerOptions.AllowAlternateSchemes = true; - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { Assert.Equal(scheme, context.Request.Scheme); return Task.CompletedTask; @@ -336,7 +349,7 @@ public async Task SchemeMismatchAllowed_InvalidScheme_Reset(string scheme) { _serviceContext.ServerOptions.AllowAlternateSchemes = true; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); var headers = new[] { new KeyValuePair(HeaderNames.Method, "GET"), new KeyValuePair(HeaderNames.Path, "/"), @@ -344,7 +357,10 @@ public async Task SchemeMismatchAllowed_InvalidScheme_Reset(string scheme) await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.FormatHttp3StreamErrorSchemeMismatch(scheme, "http")); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, + CoreStrings.FormatHttp3StreamErrorSchemeMismatch(scheme, "http")); } [Fact] @@ -357,7 +373,7 @@ public async Task MissingAuthority_200Status() new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); @@ -379,7 +395,7 @@ public async Task EmptyAuthority_200Status() new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.Authority, ""), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); @@ -402,7 +418,7 @@ public async Task MissingAuthorityFallsBackToHost_200Status() new KeyValuePair("Host", "abc"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoHost); await requestStream.SendHeadersAsync(headers, endStream: true); var responseHeaders = await requestStream.ExpectHeadersAsync(); @@ -426,7 +442,7 @@ public async Task EmptyAuthorityIgnoredOverHost_200Status() new KeyValuePair("Host", "abc"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoHost); await requestStream.SendHeadersAsync(headers, endStream: true); var responseHeaders = await requestStream.ExpectHeadersAsync(); @@ -450,7 +466,7 @@ public async Task AuthorityOverridesHost_200Status() new KeyValuePair("Host", "abc"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoHost); await requestStream.SendHeadersAsync(headers, endStream: true); var responseHeaders = await requestStream.ExpectHeadersAsync(); @@ -474,7 +490,7 @@ public async Task AuthorityOverridesInvalidHost_200Status() new KeyValuePair("Host", "a=bc"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_echoHost); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoHost); await requestStream.SendHeadersAsync(headers, endStream: true); var responseHeaders = await requestStream.ExpectHeadersAsync(); @@ -497,10 +513,12 @@ public async Task InvalidAuthority_Reset() new KeyValuePair(HeaderNames.Authority, "local=host:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("local=host:80")); } @@ -516,10 +534,12 @@ public async Task InvalidAuthorityWithValidHost_Reset() new KeyValuePair("Host", "abc"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("d=ef")); } @@ -535,10 +555,12 @@ public async Task TwoHosts_StreamReset() new KeyValuePair("Host", "host2"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("host1,host2")); } @@ -555,10 +577,12 @@ public async Task MaxRequestLineSize_Reset() new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.Authority, "localhost" + new string('a', 1024 * 3) + ":80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.RequestRejected, + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.RequestRejected, + AssertExpectedErrorMessages, CoreStrings.BadRequest_RequestLineTooLong); } @@ -573,7 +597,7 @@ public async Task ContentLength_Received_SingleDataFrame_Verified() new KeyValuePair(HeaderNames.ContentLength, "12"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var buffer = new byte[100]; var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); @@ -604,7 +628,7 @@ public async Task ContentLength_Received_MultipleDataFrame_Verified() new KeyValuePair(HeaderNames.ContentLength, "12"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var buffer = new byte[100]; var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); @@ -641,7 +665,7 @@ public async Task ContentLength_Received_MultipleDataFrame_ReadViaPipe_Verified( new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.ContentLength, "12"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var readResult = await context.Request.BodyReader.ReadAsync(); while (!readResult.IsCompleted) @@ -679,7 +703,7 @@ public async Task RemoveConnectionSpecificHeaders() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var response = context.Response; @@ -716,7 +740,7 @@ public async Task ContentLength_Received_NoDataFrames_Reset() }; var requestDelegateCalled = false; - var requestStream = await InitializeConnectionAndStreamsAsync(c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(c => { // Bad content-length + end stream means the request delegate // is never called by the server. @@ -726,7 +750,10 @@ public async Task ContentLength_Received_NoDataFrames_Reset() await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.ProtocolError, CoreStrings.Http3StreamErrorLessDataThanLength); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.ProtocolError, + AssertExpectedErrorMessages, + CoreStrings.Http3StreamErrorLessDataThanLength); Assert.False(requestDelegateCalled); } @@ -745,7 +772,7 @@ public async Task EndRequestStream_ContinueReadingFromResponse() var data = new byte[] { 1, 2, 3, 4, 5, 6 }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { await context.Response.BodyWriter.FlushAsync(); @@ -786,7 +813,7 @@ public async Task ResponseTrailers_WithoutData_Sent() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { var trailersFeature = context.Features.Get(); @@ -818,7 +845,7 @@ public async Task ResponseHeaders_WithNonAscii_Throws() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var trailersFeature = context.Features.Get(); @@ -854,7 +881,7 @@ public async Task ResponseHeaders_WithNonAsciiAndCustomEncoder_Works() _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF8; // Used for decoding response. - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var trailersFeature = context.Features.Get(); @@ -893,7 +920,7 @@ public async Task ResponseHeaders_WithInvalidValuesAndCustomEncoder_AbortsConnec DecoderFallback.ExceptionFallback); _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { context.Response.Headers.Append("CustomName", "Custom 你好 Value"); await context.Response.WriteAsync("Hello World"); @@ -901,7 +928,10 @@ public async Task ResponseHeaders_WithInvalidValuesAndCustomEncoder_AbortsConnec await requestStream.SendHeadersAsync(headers, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.InternalError, ""); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.InternalError, + AssertExpectedErrorMessages, + ""); } [Fact] @@ -915,7 +945,7 @@ public async Task ResponseTrailers_WithData_Sent() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var trailersFeature = context.Features.Get(); @@ -949,7 +979,7 @@ public async Task ResponseTrailers_WithExeption500_Cleared() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { var trailersFeature = context.Features.Get(); @@ -977,7 +1007,7 @@ public async Task ResponseTrailers_WithNonAscii_Throws() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { await context.Response.WriteAsync("Hello World"); Assert.Throws(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value")); @@ -1010,7 +1040,7 @@ public async Task ResponseTrailers_WithNonAsciiAndCustomEncoder_Works() _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => Encoding.UTF8; _serviceContext.ServerOptions.RequestHeaderEncodingSelector = _ => Encoding.UTF8; // Used for decoding response. - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { await context.Response.WriteAsync("Hello World"); Assert.Throws(() => context.Response.AppendTrailer("Custom你好Name", "Custom Value")); @@ -1051,7 +1081,7 @@ public async Task ResponseTrailers_WithInvalidValuesAndCustomEncoder_AbortsConne DecoderFallback.ExceptionFallback); _serviceContext.ServerOptions.ResponseHeaderEncodingSelector = _ => encoding; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { await context.Response.WriteAsync("Hello World"); context.Response.AppendTrailer("CustomName", "Custom 你好 Value"); @@ -1063,7 +1093,10 @@ public async Task ResponseTrailers_WithInvalidValuesAndCustomEncoder_AbortsConne var responseData = await requestStream.ExpectDataAsync(); Assert.Equal("Hello World", Encoding.ASCII.GetString(responseData.ToArray())); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.InternalError, ""); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.InternalError, + AssertExpectedErrorMessages, + ""); } [Fact] @@ -1077,7 +1110,7 @@ public async Task ResetStream_ReturnStreamError() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { var resetFeature = context.Features.Get(); @@ -1090,6 +1123,7 @@ public async Task ResetStream_ReturnStreamError() await requestStream.WaitForStreamErrorAsync( Http3ErrorCode.RequestCancelled, + AssertExpectedErrorMessages, CoreStrings.FormatHttp3StreamResetByApplication(Http3Formatting.ToFormattedErrorCode(Http3ErrorCode.RequestCancelled))); } @@ -1106,7 +1140,7 @@ public async Task CompleteAsync_BeforeBodyStarted_SendsHeadersWithEndStream() new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1154,7 +1188,7 @@ public async Task CompleteAsync_BeforeBodyStarted_WithTrailers_SendsHeadersAndTr new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1208,7 +1242,7 @@ public async Task CompleteAsync_BeforeBodyStarted_WithTrailers_TruncatedContentL new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1258,7 +1292,7 @@ public async Task CompleteAsync_AfterBodyStarted_SendsBodyWithEndStream() new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1313,7 +1347,7 @@ public async Task CompleteAsync_WriteAfterComplete_Throws() new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1364,7 +1398,7 @@ public async Task CompleteAsync_WriteAgainAfterComplete_Throws() new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1418,7 +1452,7 @@ public async Task CompleteAsync_AdvanceAfterComplete_AdvanceThrows() new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var memory = context.Response.BodyWriter.GetMemory(12); await context.Response.CompleteAsync(); @@ -1461,7 +1495,7 @@ public async Task CompleteAsync_AfterPipeWrite_WithTrailers_SendsBodyAndTrailers new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1523,7 +1557,7 @@ public async Task CompleteAsync_AfterBodyStarted_WithTrailers_SendsBodyAndTraile new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1580,7 +1614,7 @@ public async Task CompleteAsync_AfterBodyStarted_WithTrailers_TruncatedContentLe new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1640,7 +1674,7 @@ public async Task PipeWriterComplete_AfterBodyStarted_WithTrailers_TruncatedCont new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1699,7 +1733,7 @@ public async Task AbortAfterCompleteAsync_GETWithResponseBodyAndTrailers_ResetsA new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1760,7 +1794,7 @@ public async Task AbortAfterCompleteAsync_POSTWithResponseBodyAndTrailers_Reques new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1826,7 +1860,7 @@ public async Task ResetAfterCompleteAsync_GETWithResponseBodyAndTrailers_ResetsA new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1891,7 +1925,7 @@ public async Task ResetAfterCompleteAsync_POSTWithResponseBodyAndTrailers_Reques new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { try { @@ -1952,7 +1986,7 @@ await requestStream.WaitForStreamErrorAsync( [Fact] public async Task DataBeforeHeaders_UnexpectedFrameError() { - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendDataAsync(Encoding.UTF8.GetBytes("This is invalid.")); @@ -1976,7 +2010,7 @@ public async Task RequestTrailers_CanReadTrailersFromRequest() { new KeyValuePair("TestName", "TestValue"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c => { await c.Request.Body.DrainAsync(default); @@ -2008,7 +2042,7 @@ public async Task FrameAfterTrailers_UnexpectedFrameError() new KeyValuePair("TestName", "TestValue"), }; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestStream = await InitializeConnectionAndStreamsAsync(async c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c => { // Send headers await c.Response.Body.FlushAsync(); @@ -2046,7 +2080,7 @@ public async Task TrailersWithoutEndingStream_ErrorAccessingTrailers() { new KeyValuePair("TestName", "TestValue"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async c => { var data = new byte[1024]; await c.Request.Body.ReadAsync(data); @@ -2083,42 +2117,40 @@ public async Task TrailersWithoutEndingStream_ErrorAccessingTrailers() [InlineData(nameof(Http3FrameType.GoAway))] public async Task UnexpectedRequestFrame(string frameType) { - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); - var frame = new Http3RawFrame(); - frame.Type = Enum.Parse(frameType); - await requestStream.SendFrameAsync(frame, Memory.Empty); + var f = Enum.Parse(frameType); + await requestStream.SendFrameAsync(f, Memory.Empty); await requestStream.WaitForStreamErrorAsync( Http3ErrorCode.UnexpectedFrame, - expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType)); + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(Http3Formatting.ToFormattedType(f))); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: 8, expectedErrorCode: Http3ErrorCode.UnexpectedFrame, - expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(frame.FormattedType)); + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(Http3Formatting.ToFormattedType(f))); } [Theory] [InlineData(nameof(Http3FrameType.PushPromise))] public async Task UnexpectedServerFrame(string frameType) { - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); - var frame = new Http3RawFrame(); - frame.Type = Enum.Parse(frameType); - await requestStream.SendFrameAsync(frame, Memory.Empty); + var f = Enum.Parse(frameType); + await requestStream.SendFrameAsync(f, Memory.Empty); await requestStream.WaitForStreamErrorAsync( Http3ErrorCode.UnexpectedFrame, - expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(frame.FormattedType)); + expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(Http3Formatting.ToFormattedType(f))); } [Fact] public async Task RequestIncomplete() { - var requestStream = await InitializeConnectionAndStreamsAsync(_echoApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_echoApplication); await requestStream.EndStreamAsync(); @@ -2248,7 +2280,7 @@ public Task HEADERS_Received_HeaderBlockContainsDuplicatePseudoHeaderField_Conne [MemberData(nameof(ConnectMissingPseudoHeaderFieldData))] public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_MethodIsCONNECT_NoError(IEnumerable> headers) { - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); @@ -2266,11 +2298,12 @@ public Task HEADERS_Received_HeaderBlockContainsPseudoHeaderFieldAfterRegularHea private async Task HEADERS_Received_InvalidHeaderFields_StreamError(IEnumerable> headers, string expectedErrorMessage, Http3ErrorCode? errorCode = null) { - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); await requestStream.WaitForStreamErrorAsync( errorCode ?? Http3ErrorCode.MessageError, + AssertExpectedErrorMessages, expectedErrorMessage); } @@ -2278,7 +2311,7 @@ await requestStream.WaitForStreamErrorAsync( [MemberData(nameof(MissingPseudoHeaderFieldData))] public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable> headers) { - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); await requestStream.WaitForStreamErrorAsync( @@ -2380,7 +2413,7 @@ public async Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsTrailers_N new KeyValuePair("te", "trailers") }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication); await requestStream.SendHeadersAsync(headers, endStream: true); @@ -2400,7 +2433,7 @@ public async Task MaxRequestBodySize_ContentLengthUnder_200() new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.ContentLength, "12"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var buffer = new byte[100]; var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); @@ -2436,7 +2469,7 @@ public async Task MaxRequestBodySize_ContentLengthOver_413() new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.ContentLength, "12"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { #pragma warning disable CS0618 // Type or member is obsolete exception = await Assert.ThrowsAsync(async () => @@ -2477,7 +2510,7 @@ public async Task MaxRequestBodySize_NoContentLength_Under_200() new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var buffer = new byte[100]; var read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); @@ -2512,7 +2545,7 @@ public async Task MaxRequestBodySize_NoContentLength_Over_413() new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { #pragma warning disable CS0618 // Type or member is obsolete exception = await Assert.ThrowsAsync(async () => @@ -2565,7 +2598,7 @@ public async Task MaxRequestBodySize_AppCanLowerLimit(bool includeContentLength) new KeyValuePair(HeaderNames.ContentLength, "18"), }); } - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { Assert.False(context.Features.Get().IsReadOnly); context.Features.Get().MaxRequestBodySize = 17; @@ -2619,7 +2652,7 @@ public async Task MaxRequestBodySize_AppCanRaiseLimit(bool includeContentLength) new KeyValuePair(HeaderNames.ContentLength, "12"), }); } - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { Assert.False(context.Features.Get().IsReadOnly); context.Features.Get().MaxRequestBodySize = 12; @@ -2663,22 +2696,23 @@ public Task HEADERS_Received_RequestLineLength_Error() [InlineData(int.MaxValue)] public async Task UnsupportedControlStreamType(int typeId) { - await InitializeConnectionAsync(_noopApplication); + await Http3Api.InitializeConnectionAsync(_noopApplication); - var outboundControlStream = await CreateControlStream().DefaultTimeout(); + var outboundControlStream = await Http3Api.CreateControlStream().DefaultTimeout(); await outboundControlStream.SendSettingsAsync(new List()); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); // Create unsupported control stream - var invalidStream = await CreateControlStream(typeId).DefaultTimeout(); + var invalidStream = await Http3Api.CreateControlStream(typeId).DefaultTimeout(); await invalidStream.WaitForStreamErrorAsync( Http3ErrorCode.StreamCreationError, + AssertExpectedErrorMessages, CoreStrings.FormatHttp3ControlStreamErrorUnsupportedType(typeId)).DefaultTimeout(); // Connection is still alive and available for requests - var requestStream = await CreateRequestStream().DefaultTimeout(); + var requestStream = await Http3Api.CreateRequestStream().DefaultTimeout(); await requestStream.SendHeadersAsync(new[] { new KeyValuePair(HeaderNames.Path, "/"), @@ -2694,24 +2728,24 @@ await requestStream.SendHeadersAsync(new[] [Fact] public async Task HEADERS_ExceedsClientMaxFieldSectionSize_ErrorOnServer() { - await InitializeConnectionAsync(context => + await Http3Api.InitializeConnectionAsync(context => { context.Response.Headers["BigHeader"] = new string('!', 100); return Task.CompletedTask; }); - var outboundcontrolStream = await CreateControlStream(); + var outboundcontrolStream = await Http3Api.CreateControlStream(); await outboundcontrolStream.SendSettingsAsync(new List { - new Http3PeerSetting(Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100) + new Http3PeerSetting(Core.Internal.Http3.Http3SettingType.MaxFieldSectionSize, 100) }); - var maxFieldSetting = await ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); + var maxFieldSetting = await Http3Api.ServerReceivedSettingsReader.ReadAsync().DefaultTimeout(); - Assert.Equal(Internal.Http3.Http3SettingType.MaxFieldSectionSize, maxFieldSetting.Key); + Assert.Equal(Core.Internal.Http3.Http3SettingType.MaxFieldSectionSize, maxFieldSetting.Key); Assert.Equal(100, maxFieldSetting.Value); - var requestStream = await CreateRequestStream().DefaultTimeout(); + var requestStream = await Http3Api.CreateRequestStream().DefaultTimeout(); await requestStream.SendHeadersAsync(new[] { new KeyValuePair(HeaderNames.Path, "/"), @@ -2720,7 +2754,10 @@ await requestStream.SendHeadersAsync(new[] new KeyValuePair(HeaderNames.Authority, "localhost:80"), }, endStream: true); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.InternalError, "The encoded HTTP headers length exceeds the limit specified by the peer of 100 bytes."); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.InternalError, + AssertExpectedErrorMessages, + "The encoded HTTP headers length exceeds the limit specified by the peer of 100 bytes."); } [Fact] @@ -2730,7 +2767,7 @@ public async Task PostRequest_ServerReadsPartialAndFinishes_SendsBodyWithEndStre var appTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var clientTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var requestStream = await InitializeConnectionAndStreamsAsync(async context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context => { var buffer = new byte[1024]; try @@ -2804,7 +2841,7 @@ public async Task HEADERS_WriteLargeResponseHeaderSection_Success() } }); - var requestStream = await InitializeConnectionAndStreamsAsync(c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(c => { for (var i = 0; i < 10; i++) { @@ -2846,7 +2883,7 @@ public async Task HEADERS_WriteLargeResponseHeaderSectionTrailers_Success() } }); - var requestStream = await InitializeConnectionAndStreamsAsync(c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(c => { for (var i = 0; i < 10; i++) { @@ -2881,7 +2918,7 @@ public async Task HEADERS_NoResponseBody_RequestEndsOnHeaders() new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(c => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(c => { return Task.CompletedTask; }); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs index 69603d4eff17..a89eade2404a 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs @@ -44,18 +44,11 @@ public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDis protected static readonly byte[] _helloWorldBytes = Encoding.ASCII.GetBytes("hello, world"); protected static readonly byte[] _maxData = Encoding.ASCII.GetBytes(new string('a', 16 * 1024)); + internal Http3InMemory Http3Api { get; private set; } + internal TestServiceContext _serviceContext; - internal HttpConnection _httpConnection; - internal readonly TimeoutControl _timeoutControl; - internal readonly Mock _mockKestrelTrace = new Mock(); internal readonly Mock _mockTimeoutHandler = new Mock(); - internal readonly Mock _mockTimeoutControl; - internal readonly MemoryPool _memoryPool = PinnedBlockMemoryPoolFactory.Create(); - protected Task _connectionTask; - protected readonly TaskCompletionSource _closedStateReached = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - internal readonly ConcurrentDictionary _runningStreams = new ConcurrentDictionary(); - internal readonly Channel> _serverReceivedSettings; protected readonly RequestDelegate _noopApplication; protected readonly RequestDelegate _echoApplication; protected readonly RequestDelegate _readRateApplication; @@ -63,10 +56,6 @@ public abstract class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDis protected readonly RequestDelegate _echoPath; protected readonly RequestDelegate _echoHost; - protected Func OnCreateServerControlStream; - private Http3ControlStream _inboundControlStream; - private long _currentStreamId; - protected static readonly IEnumerable> _browserRequestHeaders = new[] { new KeyValuePair(HeaderNames.Method, "GET"), @@ -90,16 +79,6 @@ protected static IEnumerable> ReadRateRequestHeader public Http3TestBase() { - _timeoutControl = new TimeoutControl(_mockTimeoutHandler.Object); - _mockTimeoutControl = new Mock(_timeoutControl) { CallBase = true }; - _timeoutControl.Debugger = Mock.Of(); - - _mockKestrelTrace - .Setup(m => m.Http3ConnectionClosed(It.IsAny(), It.IsAny())) - .Callback(() => _closedStateReached.SetResult()); - - _serverReceivedSettings = Channel.CreateUnbounded>(); - _noopApplication = context => Task.CompletedTask; _echoApplication = async context => @@ -156,865 +135,34 @@ public Http3TestBase() }; } - internal Http3Connection Connection { get; private set; } - - internal Http3ControlStream OutboundControlStream { get; private set; } - - internal ChannelReader> ServerReceivedSettingsReader => _serverReceivedSettings.Reader; - - public TestMultiplexedConnectionContext MultiplexedConnectionContext { get; set; } - public override void Initialize(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper) { base.Initialize(context, methodInfo, testMethodArguments, testOutputHelper); - _serviceContext = new TestServiceContext(LoggerFactory, _mockKestrelTrace.Object) + _serviceContext = new TestServiceContext(LoggerFactory) { Scheduler = PipeScheduler.Inline, }; - } - - internal long GetStreamId(long mask) - { - var id = (_currentStreamId << 2) | mask; - _currentStreamId += 1; - - return id; + Http3Api = new Http3InMemory(_serviceContext, _serviceContext.MockSystemClock, _mockTimeoutHandler.Object); } - internal async ValueTask GetInboundControlStream() + public void AssertExpectedErrorMessages(string expectedErrorMessage) { - if (_inboundControlStream == null) + if (expectedErrorMessage != null) { - var reader = MultiplexedConnectionContext.ToClientAcceptQueue.Reader; - while (await reader.WaitToReadAsync().DefaultTimeout()) - { - while (reader.TryRead(out var stream)) - { - _inboundControlStream = stream; - var streamId = await stream.TryReadStreamIdAsync(); - - // -1 means stream was completed. - Debug.Assert(streamId == 0 || streamId == -1, "StreamId sent that was non-zero, which isn't handled by tests"); - - return _inboundControlStream; - } - } + Assert.Contains(LogMessages, m => m.Exception?.Message.Contains(expectedErrorMessage) ?? false); } - - return _inboundControlStream; } - internal void CloseConnectionGracefully() + public void AssertExpectedErrorMessages(Type exceptionType, string[] expectedErrorMessage) { - MultiplexedConnectionContext.ConnectionClosingCts.Cancel(); - } - - internal Task WaitForConnectionStopAsync(long expectedLastStreamId, bool ignoreNonGoAwayFrames, Http3ErrorCode? expectedErrorCode = null) - { - return WaitForConnectionErrorAsync(ignoreNonGoAwayFrames, expectedLastStreamId, expectedErrorCode: expectedErrorCode ?? 0, expectedErrorMessage: null); - } - - internal async Task WaitForConnectionErrorAsync(bool ignoreNonGoAwayFrames, long? expectedLastStreamId, Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage) - where TException : Exception - { - var frame = await _inboundControlStream.ReceiveFrameAsync(); - - if (ignoreNonGoAwayFrames) - { - while (frame.Type != Http3FrameType.GoAway) - { - frame = await _inboundControlStream.ReceiveFrameAsync(); - } - } - - if (expectedLastStreamId != null) - { - VerifyGoAway(frame, expectedLastStreamId.GetValueOrDefault()); - } - - AssertConnectionError(expectedErrorCode, expectedErrorMessage); - - // Verify HttpConnection.ProcessRequestsAsync has exited. - await _connectionTask.DefaultTimeout(); - - // Verify server-to-client control stream has completed. - await _inboundControlStream.ReceiveEndAsync(); - } - - internal void AssertConnectionError(Http3ErrorCode expectedErrorCode, params string[] expectedErrorMessage) where TException : Exception - { - Assert.Equal((Http3ErrorCode)expectedErrorCode, (Http3ErrorCode)MultiplexedConnectionContext.Error); - if (expectedErrorMessage?.Length > 0) { - var message = Assert.Single(LogMessages, m => m.Exception is TException); + var message = Assert.Single(LogMessages, m => m.Exception != null && exceptionType.IsAssignableFrom(m.Exception.GetType())); Assert.Contains(expectedErrorMessage, expected => message.Exception.Message.Contains(expected)); } } - - internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamId) - { - Assert.Equal(Http3FrameType.GoAway, frame.Type); - var payload = frame.Payload; - Assert.True(VariableLengthIntegerHelper.TryRead(payload.Span, out var streamId, out var _)); - 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(now); - } - - protected async Task InitializeConnectionAsync(RequestDelegate application) - { - MultiplexedConnectionContext = new TestMultiplexedConnectionContext(this); - - var httpConnectionContext = new HttpMultiplexedConnectionContext( - connectionId: "TestConnectionId", - connectionContext: MultiplexedConnectionContext, - connectionFeatures: MultiplexedConnectionContext.Features, - serviceContext: _serviceContext, - memoryPool: _memoryPool, - localEndPoint: null, - remoteEndPoint: null); - httpConnectionContext.TimeoutControl = _mockTimeoutControl.Object; - - _httpConnection = new HttpConnection(httpConnectionContext); - _httpConnection.Initialize(Connection); - _mockTimeoutHandler.Setup(h => h.OnTimeout(It.IsAny())) - .Callback(r => _httpConnection.OnTimeout(r)); - - // ProcessRequestAsync will create the Http3Connection - _connectionTask = _httpConnection.ProcessRequestsAsync(new DummyApplication(application)); - - Connection = (Http3Connection)_httpConnection._requestProcessor; - Connection._streamLifetimeHandler = new LifetimeHandlerInterceptor(Connection, this); - - await GetInboundControlStream(); - } - - internal async ValueTask InitializeConnectionAndStreamsAsync(RequestDelegate application) - { - await InitializeConnectionAsync(application); - - OutboundControlStream = await CreateControlStream(); - - return await CreateRequestStream(); - } - - private class LifetimeHandlerInterceptor : IHttp3StreamLifetimeHandler - { - private readonly IHttp3StreamLifetimeHandler _inner; - private readonly Http3TestBase _http3TestBase; - - public LifetimeHandlerInterceptor(IHttp3StreamLifetimeHandler inner, Http3TestBase http3TestBase) - { - _inner = inner; - _http3TestBase = http3TestBase; - } - - public bool OnInboundControlStream(Internal.Http3.Http3ControlStream stream) - { - return _inner.OnInboundControlStream(stream); - } - - public void OnInboundControlStreamSetting(Internal.Http3.Http3SettingType type, long value) - { - _inner.OnInboundControlStreamSetting(type, value); - - var success = _http3TestBase._serverReceivedSettings.Writer.TryWrite( - new KeyValuePair(type, value)); - Debug.Assert(success); - } - - public bool OnInboundDecoderStream(Internal.Http3.Http3ControlStream stream) - { - return _inner.OnInboundDecoderStream(stream); - } - - public bool OnInboundEncoderStream(Internal.Http3.Http3ControlStream stream) - { - return _inner.OnInboundEncoderStream(stream); - } - - public void OnStreamCompleted(IHttp3Stream stream) - { - _inner.OnStreamCompleted(stream); - - if (_http3TestBase._runningStreams.TryGetValue(stream.StreamId, out var testStream)) - { - testStream._onStreamCompletedTcs.TrySetResult(); - } - } - - public void OnStreamConnectionError(Http3ConnectionErrorException ex) - { - _inner.OnStreamConnectionError(ex); - } - - public void OnStreamCreated(IHttp3Stream stream) - { - _inner.OnStreamCreated(stream); - - if (_http3TestBase._runningStreams.TryGetValue(stream.StreamId, out var testStream)) - { - testStream._onStreamCreatedTcs.TrySetResult(); - } - } - - public void OnStreamHeaderReceived(IHttp3Stream stream) - { - _inner.OnStreamHeaderReceived(stream); - - if (_http3TestBase._runningStreams.TryGetValue(stream.StreamId, out var testStream)) - { - testStream._onHeaderReceivedTcs.TrySetResult(); - } - } - } - - protected void ConnectionClosed() - { - - } - - private static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler writerScheduler) => new PipeOptions - ( - pool: memoryPool, - readerScheduler: serviceContext.Scheduler, - writerScheduler: writerScheduler, - pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, - resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0, - useSynchronizationContext: false, - minimumSegmentSize: memoryPool.GetMinimumSegmentSize() - ); - - private static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool memoryPool, PipeScheduler readerScheduler) => new PipeOptions - ( - pool: memoryPool, - readerScheduler: readerScheduler, - writerScheduler: serviceContext.Scheduler, - pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext), - resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext), - useSynchronizationContext: false, - minimumSegmentSize: memoryPool.GetMinimumSegmentSize() - ); - - private static long GetOutputResponseBufferSize(ServiceContext serviceContext) - { - var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize; - if (bufferSize == 0) - { - // 0 = no buffering so we need to configure the pipe so the writer waits on the reader directly - return 1; - } - - // null means that we have no back pressure - return bufferSize ?? 0; - } - - public ValueTask CreateControlStream() - { - return CreateControlStream(id: 0); - } - - public async ValueTask CreateControlStream(int? id) - { - var stream = new Http3ControlStream(this, StreamInitiator.Client); - _runningStreams[stream.StreamId] = stream; - - MultiplexedConnectionContext.ToServerAcceptQueue.Writer.TryWrite(stream.StreamContext); - if (id != null) - { - await stream.WriteStreamIdAsync(id.GetValueOrDefault()); - } - return stream; - } - - internal ValueTask CreateRequestStream() - { - var stream = new Http3RequestStream(this, Connection); - _runningStreams[stream.StreamId] = stream; - - MultiplexedConnectionContext.ToServerAcceptQueue.Writer.TryWrite(stream.StreamContext); - return new ValueTask(stream); - } - - public ValueTask StartBidirectionalStreamAsync() - { - var stream = new Http3RequestStream(this, Connection); - // TODO put these somewhere to be read. - return new ValueTask(stream.StreamContext); - } - - public class Http3StreamBase : IProtocolErrorCodeFeature - { - internal TaskCompletionSource _onStreamCreatedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - internal TaskCompletionSource _onStreamCompletedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - internal TaskCompletionSource _onHeaderReceivedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - internal DuplexPipe.DuplexPipePair _pair; - internal Http3TestBase _testBase; - internal Http3Connection _connection; - public long BytesReceived { get; private set; } - public long Error { get; set; } - - public Task OnStreamCreatedTask => _onStreamCreatedTcs.Task; - public Task OnStreamCompletedTask => _onStreamCompletedTcs.Task; - public Task OnHeaderReceivedTask => _onHeaderReceivedTcs.Task; - - protected Task SendAsync(ReadOnlySpan span) - { - var writableBuffer = _pair.Application.Output; - writableBuffer.Write(span); - return FlushAsync(writableBuffer); - } - - protected static async Task FlushAsync(PipeWriter writableBuffer) - { - await writableBuffer.FlushAsync().AsTask().DefaultTimeout(); - } - - internal async Task ReceiveEndAsync() - { - var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); - Assert.True(result.IsCompleted); - } - - internal async Task ReceiveFrameAsync(bool expectEnd = false) - { - var frame = new Http3FrameWithPayload(); - - while (true) - { - var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); - var buffer = result.Buffer; - var consumed = buffer.Start; - var examined = buffer.Start; - var copyBuffer = buffer; - - try - { - Assert.True(buffer.Length > 0); - - if (Http3FrameReader.TryReadFrame(ref buffer, frame, out var framePayload)) - { - consumed = examined = framePayload.End; - frame.Payload = framePayload.ToArray(); - - if (expectEnd) - { - if (!result.IsCompleted || buffer.Length > 0) - { - throw new Exception("Reader didn't complete with frame"); - } - } - - return frame; - } - else - { - examined = buffer.End; - } - - if (result.IsCompleted) - { - throw new IOException("The reader completed without returning a frame."); - } - } - finally - { - BytesReceived += copyBuffer.Slice(copyBuffer.Start, consumed).Length; - _pair.Application.Input.AdvanceTo(consumed, examined); - } - } - } - - internal async Task SendFrameAsync(Http3RawFrame frame, Memory data, bool endStream = false) - { - var outputWriter = _pair.Application.Output; - frame.Length = data.Length; - Http3FrameWriter.WriteHeader(frame, outputWriter); - - if (!endStream) - { - await SendAsync(data.Span); - } - else - { - // Write and end stream at the same time. - // Avoid race condition of frame read separately from end of stream. - await EndStreamAsync(data.Span); - } - } - - internal Task EndStreamAsync(ReadOnlySpan span = default) - { - var writableBuffer = _pair.Application.Output; - if (span.Length > 0) - { - writableBuffer.Write(span); - } - return writableBuffer.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 - { - private readonly TestStreamContext _testStreamContext; - private readonly long _streamId; - - internal ConnectionContext StreamContext { get; } - - public bool CanRead => true; - public bool CanWrite => true; - - public long StreamId => _streamId; - - public bool Disposed => _testStreamContext.Disposed; - - private readonly byte[] _headerEncodingBuffer = new byte[64 * 1024]; - private readonly QPackDecoder _qpackDecoder = new QPackDecoder(8192); - protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public Http3RequestStream(Http3TestBase testBase, Http3Connection connection) - { - _testBase = testBase; - _connection = connection; - var inputPipeOptions = GetInputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); - var outputPipeOptions = GetOutputPipeOptions(_testBase._serviceContext, _testBase._memoryPool, PipeScheduler.ThreadPool); - - _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); - _streamId = testBase.GetStreamId(0x00); - _testStreamContext = new TestStreamContext(canRead: true, canWrite: true, _pair, this, _streamId); - StreamContext = _testStreamContext; - } - - public async Task SendHeadersAsync(IEnumerable> headers, bool endStream = false) - { - var headersTotalSize = 0; - - var frame = new Http3RawFrame(); - frame.PrepareHeaders(); - var buffer = _headerEncodingBuffer.AsMemory(); - var done = QPackHeaderWriter.BeginEncode(GetHeadersEnumerator(headers), - buffer.Span, ref headersTotalSize, out var length); - Assert.True(done); - - await SendFrameAsync(frame, buffer.Slice(0, length), endStream); - } - - internal Http3HeadersEnumerator GetHeadersEnumerator(IEnumerable> headers) - { - var dictionary = headers - .GroupBy(g => g.Key) - .ToDictionary(g => g.Key, g => new StringValues(g.Select(values => values.Value).ToArray())); - - var headersEnumerator = new Http3HeadersEnumerator(); - headersEnumerator.Initialize(dictionary); - return headersEnumerator; - } - - 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(); - frame.PrepareData(); - await SendFrameAsync(frame, data, endStream); - } - - internal async Task> ExpectHeadersAsync(bool expectEnd = false) - { - var http3WithPayload = await ReceiveFrameAsync(expectEnd); - Assert.Equal(Http3FrameType.Headers, http3WithPayload.Type); - - _decodedHeaders.Clear(); - _qpackDecoder.Decode(http3WithPayload.PayloadSequence, this); - _qpackDecoder.Reset(); - return _decodedHeaders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, _decodedHeaders.Comparer); - } - - internal async Task> ExpectDataAsync() - { - var http3WithPayload = await ReceiveFrameAsync(); - return http3WithPayload.Payload; - } - - internal async Task ExpectReceiveEndOfStream() - { - var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); - Assert.True(result.IsCompleted); - } - - public void OnHeader(ReadOnlySpan name, ReadOnlySpan value) - { - _decodedHeaders[name.GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); - } - - public void OnHeadersComplete(bool endHeaders) - { - } - - public void OnStaticIndexedHeader(int index) - { - var knownHeader = H3StaticTable.GetHeaderFieldAt(index); - _decodedHeaders[((Span)knownHeader.Name).GetAsciiStringNonNullCharacters()] = HttpUtilities.GetAsciiOrUTF8StringNonNullCharacters((ReadOnlySpan)knownHeader.Value); - } - - public void OnStaticIndexedHeader(int index, ReadOnlySpan value) - { - _decodedHeaders[((Span)H3StaticTable.GetHeaderFieldAt(index).Name).GetAsciiStringNonNullCharacters()] = value.GetAsciiOrUTF8StringNonNullCharacters(); - } - } - - internal class Http3FrameWithPayload : Http3RawFrame - { - public Http3FrameWithPayload() : base() - { - } - - // This does not contain extended headers - public Memory Payload { get; set; } - - public ReadOnlySequence PayloadSequence => new ReadOnlySequence(Payload); - } - - public enum StreamInitiator - { - Client, - Server - } - - public class Http3ControlStream : Http3StreamBase - { - internal ConnectionContext StreamContext { get; } - private readonly long _streamId; - - public bool CanRead => true; - public bool CanWrite => false; - - public long StreamId => _streamId; - - 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); - _streamId = testBase.GetStreamId(initiator == StreamInitiator.Client ? 0x02 : 0x03); - StreamContext = new TestStreamContext(canRead: true, canWrite: false, _pair, this, _streamId); - } - - public Http3ControlStream(ConnectionContext streamContext) - { - StreamContext = streamContext; - } - - internal async Task> ExpectSettingsAsync() - { - var http3WithPayload = await ReceiveFrameAsync(); - Assert.Equal(Http3FrameType.Settings, http3WithPayload.Type); - - var payload = http3WithPayload.PayloadSequence; - - var settings = new Dictionary(); - while (true) - { - var id = VariableLengthIntegerHelper.GetInteger(payload, out var consumed, out _); - if (id == -1) - { - break; - } - - payload = payload.Slice(consumed); - - var value = VariableLengthIntegerHelper.GetInteger(payload, out consumed, out _); - if (value == -1) - { - break; - } - - payload = payload.Slice(consumed); - settings.Add(id, value); - } - - return settings; - } - - public async Task WriteStreamIdAsync(int id) - { - var writableBuffer = _pair.Application.Output; - - void WriteSpan(PipeWriter pw) - { - var buffer = pw.GetSpan(sizeHint: 8); - var lengthWritten = VariableLengthIntegerHelper.WriteInteger(buffer, id); - pw.Advance(lengthWritten); - } - - WriteSpan(writableBuffer); - - await FlushAsync(writableBuffer); - } - - internal async Task SendGoAwayAsync(long streamId, bool endStream = false) - { - var frame = new Http3RawFrame(); - frame.PrepareGoAway(); - - var data = new byte[VariableLengthIntegerHelper.GetByteCount(streamId)]; - VariableLengthIntegerHelper.WriteInteger(data, streamId); - - await SendFrameAsync(frame, data, endStream); - } - - internal async Task SendSettingsAsync(List settings, bool endStream = false) - { - var frame = new Http3RawFrame(); - frame.PrepareSettings(); - - var settingsLength = CalculateSettingsSize(settings); - var buffer = new byte[settingsLength]; - WriteSettings(settings, buffer); - - await SendFrameAsync(frame, buffer, endStream); - } - - internal static int CalculateSettingsSize(List settings) - { - var length = 0; - foreach (var setting in settings) - { - length += VariableLengthIntegerHelper.GetByteCount((long)setting.Parameter); - length += VariableLengthIntegerHelper.GetByteCount(setting.Value); - } - return length; - } - - internal static void WriteSettings(List settings, Span destination) - { - foreach (var setting in settings) - { - var parameterLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Parameter); - destination = destination.Slice(parameterLength); - - var valueLength = VariableLengthIntegerHelper.WriteInteger(destination, (long)setting.Value); - destination = destination.Slice(valueLength); - } - } - - public async ValueTask TryReadStreamIdAsync() - { - while (true) - { - var result = await _pair.Application.Input.ReadAsync(); - var readableBuffer = result.Buffer; - var consumed = readableBuffer.Start; - var examined = readableBuffer.End; - - try - { - if (!readableBuffer.IsEmpty) - { - var id = VariableLengthIntegerHelper.GetInteger(readableBuffer, out consumed, out examined); - if (id != -1) - { - return id; - } - } - - if (result.IsCompleted) - { - return -1; - } - } - finally - { - _pair.Application.Input.AdvanceTo(consumed, examined); - } - } - } - } - - public class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, IProtocolErrorCodeFeature - { - public readonly Channel ToServerAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions - { - SingleReader = true, - SingleWriter = true - }); - - public readonly Channel ToClientAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions - { - SingleReader = true, - SingleWriter = true - }); - - private readonly Http3TestBase _testBase; - private long _error; - - public TestMultiplexedConnectionContext(Http3TestBase testBase) - { - _testBase = testBase; - Features = new FeatureCollection(); - Features.Set(this); - Features.Set(this); - Features.Set(this); - ConnectionClosedRequested = ConnectionClosingCts.Token; - } - - public override string ConnectionId { get; set; } - - public override IFeatureCollection Features { get; } - - public override IDictionary Items { get; set; } - - public CancellationToken ConnectionClosedRequested { get; set; } - - public CancellationTokenSource ConnectionClosingCts { get; set; } = new CancellationTokenSource(); - - public long Error - { - get => _error; - set => _error = value; - } - - public override void Abort() - { - Abort(new ConnectionAbortedException()); - } - - public override void Abort(ConnectionAbortedException abortReason) - { - ToServerAcceptQueue.Writer.TryComplete(); - ToClientAcceptQueue.Writer.TryComplete(); - } - - public override async ValueTask AcceptAsync(CancellationToken cancellationToken = default) - { - while (await ToServerAcceptQueue.Reader.WaitToReadAsync()) - { - while (ToServerAcceptQueue.Reader.TryRead(out var connection)) - { - return connection; - } - } - - return null; - } - - public override ValueTask ConnectAsync(IFeatureCollection features = null, CancellationToken cancellationToken = default) - { - var stream = _testBase.OnCreateServerControlStream?.Invoke() ?? new Http3ControlStream(_testBase, StreamInitiator.Server); - ToClientAcceptQueue.Writer.WriteAsync(stream); - return new ValueTask(stream.StreamContext); - } - - public void OnHeartbeat(Action action, object state) - { - } - - public void RequestClose() - { - throw new NotImplementedException(); - } - } - - private class TestStreamContext : ConnectionContext, IStreamDirectionFeature, IStreamIdFeature - { - private readonly DuplexPipePair _pair; - 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(errorCodeFeature); - - CanRead = canRead; - CanWrite = canWrite; - StreamId = streamId; - } - - public bool Disposed { get; private set; } - - public override string ConnectionId { get; set; } - - public long StreamId { get; } - - public override IFeatureCollection Features { get; } - - public override IDictionary Items { get; set; } - - public override IDuplexPipe Transport - { - get - { - return _pair.Transport; - } - set - { - throw new NotImplementedException(); - } - } - - public bool CanRead { get; } - - public bool CanWrite { get; } - - public override void Abort(ConnectionAbortedException abortReason) - { - _pair.Application.Output.Complete(abortReason); - } - - public override ValueTask DisposeAsync() - { - Disposed = true; - return base.DisposeAsync(); - } - } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs index 59153fd8dc0d..9ee3cd134678 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs @@ -26,25 +26,28 @@ public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_Str var now = _serviceContext.MockSystemClock.UtcNow; var limits = _serviceContext.ServerOptions.Limits; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout(); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout(); - var controlStream = await GetInboundControlStream().DefaultTimeout(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); await requestStream.OnStreamCreatedTask.DefaultTimeout(); - var serverRequestStream = Connection._streams[requestStream.StreamId]; + var serverRequestStream = Http3Api.Connection._streams[requestStream.StreamId]; await requestStream.SendHeadersPartialAsync().DefaultTimeout(); - TriggerTick(now); - TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(now); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.HeaderTimeoutTicks); - TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); - await requestStream.WaitForStreamErrorAsync(Http3ErrorCode.RequestRejected, CoreStrings.BadRequest_RequestHeadersTimeout); + await requestStream.WaitForStreamErrorAsync( + Http3ErrorCode.RequestRejected, + AssertExpectedErrorMessages, + CoreStrings.BadRequest_RequestHeadersTimeout); } [Fact] @@ -60,17 +63,17 @@ public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout(); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication).DefaultTimeout(); - var controlStream = await GetInboundControlStream().DefaultTimeout(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); await requestStream.OnStreamCreatedTask.DefaultTimeout(); - var serverRequestStream = Connection._streams[requestStream.StreamId]; + var serverRequestStream = Http3Api.Connection._streams[requestStream.StreamId]; - TriggerTick(now); - TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(now); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverRequestStream.HeaderTimeoutTicks); @@ -78,7 +81,7 @@ public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success await requestStream.OnHeaderReceivedTask.DefaultTimeout(); - TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); await requestStream.SendDataAsync(Memory.Empty, endStream: true); @@ -100,25 +103,28 @@ public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_Str new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - var controlStream = await GetInboundControlStream().DefaultTimeout(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); - var outboundControlStream = await CreateControlStream(id: null); + var outboundControlStream = await Http3Api.CreateControlStream(id: null); await outboundControlStream.OnStreamCreatedTask.DefaultTimeout(); - var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId]; + var serverInboundControlStream = Http3Api.Connection._streams[outboundControlStream.StreamId]; - TriggerTick(now); - TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(now); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); Assert.Equal((now + limits.RequestHeadersTimeout).Ticks, serverInboundControlStream.HeaderTimeoutTicks); - TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); - await outboundControlStream.WaitForStreamErrorAsync(Http3ErrorCode.StreamCreationError, CoreStrings.Http3ControlStreamHeaderTimeout); + await outboundControlStream.WaitForStreamErrorAsync( + Http3ErrorCode.StreamCreationError, + AssertExpectedErrorMessages, + CoreStrings.Http3ControlStreamHeaderTimeout); } [Fact] @@ -134,23 +140,23 @@ public async Task ControlStream_HeaderReceivedWithinRequestHeadersTimeout_Stream new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - var controlStream = await GetInboundControlStream().DefaultTimeout(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); - var outboundControlStream = await CreateControlStream(id: null); + var outboundControlStream = await Http3Api.CreateControlStream(id: null); await outboundControlStream.OnStreamCreatedTask.DefaultTimeout(); - TriggerTick(now); - TriggerTick(now + limits.RequestHeadersTimeout); + Http3Api.TriggerTick(now); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout); await outboundControlStream.WriteStreamIdAsync(id: 0); await outboundControlStream.OnHeaderReceivedTask.DefaultTimeout(); - TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); + Http3Api.TriggerTick(now + limits.RequestHeadersTimeout + TimeSpan.FromTicks(1)); } [Fact] @@ -168,18 +174,18 @@ public async Task ControlStream_RequestHeadersTimeoutMaxValue_ExpirationIsMaxVal new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - await InitializeConnectionAsync(_noopApplication).DefaultTimeout(); + await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout(); - var controlStream = await GetInboundControlStream().DefaultTimeout(); + var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout(); await controlStream.ExpectSettingsAsync().DefaultTimeout(); - var outboundControlStream = await CreateControlStream(id: null); + var outboundControlStream = await Http3Api.CreateControlStream(id: null); await outboundControlStream.OnStreamCreatedTask.DefaultTimeout(); - var serverInboundControlStream = Connection._streams[outboundControlStream.StreamId]; + var serverInboundControlStream = Http3Api.Connection._streams[outboundControlStream.StreamId]; - TriggerTick(now); + Http3Api.TriggerTick(now); Assert.Equal(TimeSpan.MaxValue.Ticks, serverInboundControlStream.HeaderTimeoutTicks); } @@ -193,11 +199,11 @@ public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGraceP // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); - var requestStream = await InitializeConnectionAndStreamsAsync(_readRateApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_readRateApplication); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); // _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period. @@ -209,15 +215,15 @@ public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGraceP await requestStream.ExpectDataAsync(); // Don't send any more data and advance just to and then past the grace period. - AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + Http3Api.AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + Http3Api.AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: 8, Http3ErrorCode.InternalError, @@ -297,10 +303,10 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC // Disable response buffering so "socket" backpressure is observed immediately. limits.MaxResponseBufferSize = 0; - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); var app = new EchoAppWithNotification(); - var requestStream = await InitializeConnectionAndStreamsAsync(app.RunApp); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(app.RunApp); await requestStream.SendHeadersAsync(_browserRequestHeaders, endStream: false); await requestStream.SendDataAsync(_helloWorldBytes, endStream: true); @@ -310,15 +316,15 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC await app.WriteStartedTask.DefaultTimeout(); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. - _timeoutControl.Tick(mockSystemClock.UtcNow); + Http3Api._timeoutControl.Tick(mockSystemClock.UtcNow); // Don't read data frame to induce "socket" backpressure. - AdvanceClock(TimeSpan.FromSeconds((requestStream.BytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + + Http3Api.AdvanceClock(TimeSpan.FromSeconds((requestStream.BytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -341,10 +347,10 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC // Disable response buffering so "socket" backpressure is observed immediately. limits.MaxResponseBufferSize = 0; - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); var app = new EchoAppWithNotification(); - var requestStream = await InitializeConnectionAndStreamsAsync(app.RunApp); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(app.RunApp); await requestStream.SendHeadersAsync(_browserRequestHeaders, endStream: false); await requestStream.SendDataAsync(_maxData, endStream: true); @@ -354,17 +360,17 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC await app.WriteStartedTask.DefaultTimeout(); // Complete timing of the request body so we don't induce any unexpected request body rate timeouts. - _timeoutControl.Tick(mockSystemClock.UtcNow); + Http3Api._timeoutControl.Tick(mockSystemClock.UtcNow); var timeToWriteMaxData = TimeSpan.FromSeconds((requestStream.BytesReceived + _maxData.Length) / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5); // Don't read data frame to induce "socket" backpressure. - AdvanceClock(timeToWriteMaxData); + Http3Api.AdvanceClock(timeToWriteMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once); @@ -383,11 +389,11 @@ public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTi // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); - var requestStream = await InitializeConnectionAndStreamsAsync(_readRateApplication); + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_readRateApplication); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period. @@ -403,15 +409,15 @@ public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTi var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - AdvanceClock(timeToReadMaxData); + Http3Api.AdvanceClock(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: null, Http3ErrorCode.InternalError, @@ -429,14 +435,14 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); - await InitializeConnectionAsync(_readRateApplication); + await Http3Api.InitializeConnectionAsync(_readRateApplication); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); - var requestStream1 = await CreateRequestStream(); + var requestStream1 = await Http3Api.CreateRequestStream(); // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period. await requestStream1.SendHeadersAsync(ReadRateRequestHeaders(_maxData.Length), endStream: false); @@ -445,7 +451,7 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter await requestStream1.ExpectHeadersAsync(); await requestStream1.ExpectDataAsync(); - var requestStream2 = await CreateRequestStream(); + var requestStream2 = await Http3Api.CreateRequestStream(); await requestStream2.SendHeadersAsync(ReadRateRequestHeaders(_maxData.Length), endStream: false); await requestStream2.SendDataAsync(_maxData, endStream: false); @@ -462,15 +468,15 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter timeToReadMaxData -= TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - AdvanceClock(timeToReadMaxData); + Http3Api.AdvanceClock(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: null, Http3ErrorCode.InternalError, @@ -488,14 +494,14 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); - await InitializeConnectionAsync(_readRateApplication); + await Http3Api.InitializeConnectionAsync(_readRateApplication); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); - var requestStream1 = await CreateRequestStream(); + var requestStream1 = await Http3Api.CreateRequestStream(); // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period. await requestStream1.SendHeadersAsync(ReadRateRequestHeaders(_maxData.Length), endStream: false); @@ -506,7 +512,7 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon await requestStream1.ExpectReceiveEndOfStream(); - var requestStream2 = await CreateRequestStream(); + var requestStream2 = await Http3Api.CreateRequestStream(); await requestStream2.SendHeadersAsync(ReadRateRequestHeaders(_maxData.Length), endStream: false); await requestStream2.SendDataAsync(_maxData, endStream: false); @@ -519,15 +525,15 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5); // Don't send any more data and advance just to and then past the rate timeout. - AdvanceClock(timeToReadMaxData); + Http3Api.AdvanceClock(timeToReadMaxData); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromSeconds(1)); + Http3Api.AdvanceClock(TimeSpan.FromSeconds(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once); - await WaitForConnectionErrorAsync( + await Http3Api.WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: false, expectedLastStreamId: null, Http3ErrorCode.InternalError, @@ -545,16 +551,16 @@ public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbo // Use non-default value to ensure the min request and response rates aren't mixed up. limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5)); - _timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); + Http3Api._timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); - var requestStream = await InitializeConnectionAndStreamsAsync(context => + var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(context => { // Completely disable rate limiting for this stream. context.Features.Get().MinDataRate = null; return _readRateApplication(context); }); - var inboundControlStream = await GetInboundControlStream(); + var inboundControlStream = await Http3Api.GetInboundControlStream(); await inboundControlStream.ExpectSettingsAsync(); // _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period. @@ -566,11 +572,11 @@ public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbo await requestStream.ExpectDataAsync(); // Don't send any more data and advance just to and then past the grace period. - AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); + Http3Api.AdvanceClock(limits.MinRequestBodyDataRate.GracePeriod); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); - AdvanceClock(TimeSpan.FromTicks(1)); + Http3Api.AdvanceClock(TimeSpan.FromTicks(1)); _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny()), Times.Never); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj index ad4e803208a8..82be1b56ae04 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/InMemory.FunctionalTests.csproj @@ -5,17 +5,21 @@ true InMemory.FunctionalTests true + $(DefineConstants);IS_FUNCTIONAL_TESTS + + + diff --git a/src/Shared/BenchmarkRunner/Program.cs b/src/Shared/BenchmarkRunner/Program.cs index bf167da0dc8f..82f4a77f37b1 100644 --- a/src/Shared/BenchmarkRunner/Program.cs +++ b/src/Shared/BenchmarkRunner/Program.cs @@ -81,7 +81,7 @@ private static void AssignConfiguration(ref string[] args) } var index = argsList.IndexOf("--config"); - if (index >= 0 && index < argsList.Count -1) + if (index >= 0 && index < argsList.Count - 1) { AspNetCoreBenchmarkAttribute.ConfigName = argsList[index + 1]; argsList.RemoveAt(index + 1);