diff --git a/src/Servers/Kestrel/Core/src/Internal/BaseHttpConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/BaseHttpConnectionContext.cs index 24ec4a560d72..5a99fb872410 100644 --- a/src/Servers/Kestrel/Core/src/Internal/BaseHttpConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/BaseHttpConnectionContext.cs @@ -20,7 +20,8 @@ internal class BaseHttpConnectionContext IFeatureCollection connectionFeatures, MemoryPool memoryPool, IPEndPoint? localEndPoint, - IPEndPoint? remoteEndPoint) + IPEndPoint? remoteEndPoint, + ConnectionMetricsContext metricsContext) { ConnectionId = connectionId; Protocols = protocols; @@ -31,6 +32,7 @@ internal class BaseHttpConnectionContext MemoryPool = memoryPool; LocalEndPoint = localEndPoint; RemoteEndPoint = remoteEndPoint; + MetricsContext = metricsContext; } public string ConnectionId { get; set; } @@ -42,6 +44,7 @@ internal class BaseHttpConnectionContext public MemoryPool MemoryPool { get; } public IPEndPoint? LocalEndPoint { get; } public IPEndPoint? RemoteEndPoint { get; } + public ConnectionMetricsContext MetricsContext { get; } public ITimeoutControl TimeoutControl { get; set; } = default!; // Always set by HttpConnection public ExecutionContext? InitialExecutionContext { get; set; } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 48c9e0595fae..58cf63bee2e5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -62,6 +62,7 @@ public Http1Connection(HttpConnectionContext context) _context.ServiceContext.Log, _context.TimeoutControl, minResponseDataRateFeature: this, + MetricsContext, outputAborter: this); Input = _context.Transport.Input; @@ -69,6 +70,8 @@ public Http1Connection(HttpConnectionContext context) MemoryPool = _context.MemoryPool; } + public ConnectionMetricsContext MetricsContext => _context.MetricsContext; + public PipeReader Input { get; } public bool RequestTimedOut => _requestTimedOut; @@ -82,7 +85,7 @@ protected override void OnRequestProcessingEnded() if (IsUpgraded) { KestrelEventSource.Log.RequestUpgradedStop(this); - ServiceContext.Metrics.RequestUpgradedStop(_context.MetricsContext); + ServiceContext.Metrics.RequestUpgradedStop(MetricsContext); ServiceContext.ConnectionManager.UpgradedConnectionCount.ReleaseOne(); } @@ -98,22 +101,22 @@ protected override void OnRequestProcessingEnded() void IRequestProcessor.OnInputOrOutputCompleted() { // Closed gracefully. - _http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); + _http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!, ConnectionEndReason.TransportCompleted); CancelRequestAbortedToken(); } void IHttpOutputAborter.OnInputOrOutputCompleted() { - _http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + _http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), ConnectionEndReason.TransportCompleted); CancelRequestAbortedToken(); } /// /// Immediately kill the connection and poison the request body stream with an error. /// - public void Abort(ConnectionAbortedException abortReason) + public void Abort(ConnectionAbortedException abortReason, ConnectionEndReason reason) { - _http1Output.Abort(abortReason); + _http1Output.Abort(abortReason, reason); CancelRequestAbortedToken(); PoisonBody(abortReason); } @@ -121,7 +124,7 @@ public void Abort(ConnectionAbortedException abortReason) protected override void ApplicationAbort() { Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier); - Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication)); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication), ConnectionEndReason.AbortedByApp); } /// @@ -129,8 +132,10 @@ protected override void ApplicationAbort() /// Called on all active connections when the server wants to initiate a shutdown /// and after a keep-alive timeout. /// - public void StopProcessingNextRequest() + public void StopProcessingNextRequest(ConnectionEndReason reason) { + KestrelMetrics.AddConnectionEndReason(MetricsContext, reason); + _keepAlive = false; Input.CancelPendingRead(); } @@ -142,12 +147,16 @@ public void SendTimeoutResponse() } public void HandleRequestHeadersTimeout() - => SendTimeoutResponse(); + { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.RequestHeadersTimeout); + SendTimeoutResponse(); + } public void HandleReadDataRateTimeout() { Debug.Assert(MinRequestBodyDataRate != null); + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.MinRequestBodyDataRate); Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, TraceIdentifier, MinRequestBodyDataRate.BytesPerSecond); SendTimeoutResponse(); } @@ -701,17 +710,22 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio } catch (InvalidOperationException) when (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders) { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); throw; } #pragma warning disable CS0618 // Type or member is obsolete catch (BadHttpRequestException ex) { - DetectHttp2Preface(result.Buffer, ex); - + OnBadRequest(result.Buffer, ex); throw; } #pragma warning restore CS0618 // Type or member is obsolete + catch (Exception) + { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.OtherError); + throw; + } finally { Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End); @@ -725,9 +739,11 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio endConnection = true; return true; case RequestProcessingStatus.ParsingRequestLine: + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine); break; case RequestProcessingStatus.ParsingHeaders: + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders); break; } @@ -743,6 +759,7 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio { // In this case, there is an ongoing request but the start line/header parsing has timed out, so send // a 408 response. + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.RequestHeadersTimeout); KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout); } @@ -759,8 +776,53 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio } #pragma warning disable CS0618 // Type or member is obsolete - private void DetectHttp2Preface(ReadOnlySequence requestData, BadHttpRequestException ex) + private void OnBadRequest(ReadOnlySequence requestData, BadHttpRequestException ex) #pragma warning restore CS0618 // Type or member is obsolete + { + var reason = ex.Reason; + + // Some code shared between HTTP versions throws errors. For example, HttpRequestHeaders collection + // throws when an invalid content length is set. + // Only want to set a reasons for HTTP/1.1 connection, so set end reason by catching the exception here. + switch (reason) + { + case RequestRejectionReason.UnrecognizedHTTPVersion: + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidHttpVersion); + DetectHttp2Preface(requestData); + break; + case RequestRejectionReason.InvalidRequestLine: + case RequestRejectionReason.RequestLineTooLong: + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestLine); + break; + case RequestRejectionReason.InvalidRequestHeadersNoCRLF: + case RequestRejectionReason.InvalidRequestHeader: + case RequestRejectionReason.InvalidContentLength: + case RequestRejectionReason.HeadersExceedMaxTotalSize: + case RequestRejectionReason.TooManyHeaders: + case RequestRejectionReason.MultipleContentLengths: + case RequestRejectionReason.MalformedRequestInvalidHeaders: + case RequestRejectionReason.InvalidRequestTarget: + case RequestRejectionReason.InvalidCharactersInHeaderName: + case RequestRejectionReason.LengthRequiredHttp10: + case RequestRejectionReason.OptionsMethodRequired: + case RequestRejectionReason.ConnectMethodRequired: + case RequestRejectionReason.MissingHostHeader: + case RequestRejectionReason.MultipleHostHeaders: + case RequestRejectionReason.InvalidHostHeader: + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); + break; + case RequestRejectionReason.TlsOverHttpError: + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.TlsOverHttp); + break; + default: + // In some scenarios the end reason might already be set to a more specific error + // and attempting to set the reason again has no impact. + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.OtherError); + break; + } + } + + private void DetectHttp2Preface(ReadOnlySequence requestData) { const int PrefaceLineLength = 16; @@ -770,8 +832,7 @@ private void DetectHttp2Preface(ReadOnlySequence requestData, BadHttpReque { // If there is an unrecognized HTTP version, it is the first request on the connection, and the request line // bytes matches the HTTP/2 preface request line bytes then log and return a HTTP/2 GOAWAY frame. - if (ex.Reason == RequestRejectionReason.UnrecognizedHTTPVersion - && _requestCount == 1 + if (_requestCount == 1 && requestData.Length >= PrefaceLineLength) { var clientPrefaceRequestLine = Http2.Http2Connection.ClientPreface.Slice(0, PrefaceLineLength); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs index 69a7c2a5d8ca..adfc36ab4cb8 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs @@ -7,6 +7,7 @@ using System.IO.Pipelines; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -247,6 +248,7 @@ protected override void OnReadStarting() if (_contentLength > maxRequestBodySize) { _context.DisableHttp1KeepAlive(); + KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.MaxRequestBodySizeExceeded); KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)); } } @@ -269,6 +271,7 @@ private void VerifyIsNotReading() { if (_readResult.IsCompleted) { + KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.UnexpectedEndOfRequestContent); KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index dbdf2a3f5fc2..d6adf36bdd3c 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using System.IO.Pipelines; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; @@ -68,11 +69,10 @@ protected override Task OnConsumeAsync() } catch (InvalidOperationException ex) { - var connectionAbortedException = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication, ex); - _context.ReportApplicationError(connectionAbortedException); + Log.RequestBodyDrainBodyReaderInvalidState(_context.ConnectionIdFeature, _context.TraceIdentifier, ex); // Have to abort the connection because we can't finish draining the request - _context.StopProcessingNextRequest(); + _context.StopProcessingNextRequest(ConnectionEndReason.BodyReaderInvalidState); return Task.CompletedTask; } @@ -104,11 +104,10 @@ protected async Task OnConsumeAsyncAwaited() } catch (InvalidOperationException ex) { - var connectionAbortedException = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication, ex); - _context.ReportApplicationError(connectionAbortedException); + Log.RequestBodyDrainBodyReaderInvalidState(_context.ConnectionIdFeature, _context.TraceIdentifier, ex); // Have to abort the connection because we can't finish draining the request - _context.StopProcessingNextRequest(); + _context.StopProcessingNextRequest(ConnectionEndReason.BodyReaderInvalidState); } finally { @@ -116,6 +115,13 @@ protected async Task OnConsumeAsyncAwaited() } } + protected override void OnOnbservedBytesExceedMaxRequestBodySize(long? maxRequestBodySize) + { + _context.DisableHttp1KeepAlive(); + KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.MaxRequestBodySizeExceeded); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)); + } + public static MessageBody For( HttpVersion httpVersion, HttpRequestHeaders headers, @@ -228,6 +234,7 @@ protected void ThrowUnexpectedEndOfRequestContent() // closing the connection without a response as expected. ((IHttpOutputAborter)_context).OnInputOrOutputCompleted(); + KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.UnexpectedEndOfRequestContent); KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs index c32d6940f3c7..47d74bdaa6c6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs @@ -29,6 +29,7 @@ internal class Http1OutputProducer : IHttpOutputProducer, IDisposable private readonly MemoryPool _memoryPool; private readonly KestrelTrace _log; private readonly IHttpMinResponseDataRateFeature _minResponseDataRateFeature; + private readonly ConnectionMetricsContext _connectionMetricsContext; private readonly IHttpOutputAborter _outputAborter; private readonly TimingPipeFlusher _flusher; @@ -74,6 +75,7 @@ internal class Http1OutputProducer : IHttpOutputProducer, IDisposable KestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature, + ConnectionMetricsContext connectionMetricsContext, IHttpOutputAborter outputAborter) { // Allow appending more data to the PipeWriter when a flush is pending. @@ -83,6 +85,7 @@ internal class Http1OutputProducer : IHttpOutputProducer, IDisposable _memoryPool = memoryPool; _log = log; _minResponseDataRateFeature = minResponseDataRateFeature; + _connectionMetricsContext = connectionMetricsContext; _outputAborter = outputAborter; _flusher = new TimingPipeFlusher(timeoutControl, log); @@ -444,7 +447,7 @@ private void CompletePipe() } } - public void Abort(ConnectionAbortedException error) + public void Abort(ConnectionAbortedException error, ConnectionEndReason reason) { // Abort can be called after Dispose if there's a flush timeout. // It's important to still call _lifetimeFeature.Abort() in this case. @@ -455,6 +458,8 @@ public void Abort(ConnectionAbortedException error) return; } + KestrelMetrics.AddConnectionEndReason(_connectionMetricsContext, reason); + _aborted = true; _connectionContext.Abort(error); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputAborter.cs b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputAborter.cs index b211893465c9..8fbc1396a307 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputAborter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/IHttpOutputAborter.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; internal interface IHttpOutputAborter { - void Abort(ConnectionAbortedException abortReason); + void Abort(ConnectionAbortedException abortReason, ConnectionEndReason reason); void OnInputOrOutputCompleted(); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs index 9f0980cd4cda..f9e80c8fb169 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/MessageBody.cs @@ -191,11 +191,15 @@ protected void AddAndCheckObservedBytes(long observedBytes) var maxRequestBodySize = _context.MaxRequestBodySize; if (_observedBytes > maxRequestBodySize) { - _context.DisableHttp1KeepAlive(); - KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)); + OnOnbservedBytesExceedMaxRequestBodySize(maxRequestBodySize); } } + protected virtual void OnOnbservedBytesExceedMaxRequestBodySize(long? maxRequestBodySize) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)); + } + protected ValueTask StartTimingReadAsync(ValueTask readAwaitable, CancellationToken cancellationToken) { if (!readAwaitable.IsCompleted) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs b/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs index 0194f09f16d6..827192823023 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/RequestRejectionReason.cs @@ -31,6 +31,5 @@ internal enum RequestRejectionReason ConnectMethodRequired, MissingHostHeader, MultipleHostHeaders, - InvalidHostHeader, - RequestBodyExceedsContentLength + InvalidHostHeader } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/FlowControl/InputFlowControl.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/FlowControl/InputFlowControl.cs index 91d40f12f406..afabff46e6d9 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/FlowControl/InputFlowControl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/FlowControl/InputFlowControl.cs @@ -41,7 +41,7 @@ public bool TryAdvance(int bytes) // flow-control window at the time of the abort. if (bytes > _flow.Available) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorFlowControlWindowExceeded, Http2ErrorCode.FLOW_CONTROL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorFlowControlWindowExceeded, Http2ErrorCode.FLOW_CONTROL_ERROR, ConnectionEndReason.FlowControlWindowExceeded); } if (_flow.IsAborted) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index 890cf2aed347..9c6c380a8e98 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -87,6 +87,8 @@ private static int GetMaximumEnhanceYourCalmCount() private readonly HttpConnectionContext _context; private readonly ConnectionMetricsContext _metricsContext; + private readonly IProtocolErrorCodeFeature _errorCodeFeature; + private readonly IConnectionMetricsTagsFeature? _metricsTagsFeature; private readonly Http2FrameWriter _frameWriter; private readonly Pipe _input; private readonly Task _inputTask; @@ -123,6 +125,7 @@ private static int GetMaximumEnhanceYourCalmCount() private readonly ConcurrentQueue _completedStreams = new ConcurrentQueue(); private readonly StreamCloseAwaitable _streamCompletionAwaitable = new StreamCloseAwaitable(); private int _gracefulCloseInitiator; + private ConnectionEndReason _gracefulCloseReason; private int _isClosed; // Internal for testing @@ -138,7 +141,9 @@ public Http2Connection(HttpConnectionContext context) _context = context; _streamLifetimeHandler = this; - _metricsContext = context.ConnectionFeatures.GetRequiredFeature().MetricsContext; + _metricsContext = context.MetricsContext; + _errorCodeFeature = context.ConnectionFeatures.GetRequiredFeature(); + _metricsTagsFeature = context.ConnectionFeatures.Get(); // Capture the ExecutionContext before dispatching HTTP/2 middleware. Will be restored by streams when processing request _context.InitialExecutionContext = ExecutionContext.Capture(); @@ -204,28 +209,47 @@ public Http2Connection(HttpConnectionContext context) public void OnInputOrOutputCompleted() { - TryClose(); - var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0; + var hasActiveStreams = _clientActiveStreamCount != 0; + if (TryClose()) + { + SetConnectionErrorCode(hasActiveStreams ? ConnectionEndReason.ConnectionReset : ConnectionEndReason.TransportCompleted, Http2ErrorCode.NO_ERROR); + } + var useException = _context.ServiceContext.ServerOptions.FinOnError || hasActiveStreams; _frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); } - public void Abort(ConnectionAbortedException ex) + private void SetConnectionErrorCode(ConnectionEndReason reason, Http2ErrorCode errorCode) + { + Debug.Assert(_isClosed == 1, "Should only be set when connection is closed."); + Debug.Assert(_errorCodeFeature.Error == -1, "Error code feature should only be set once."); + + _errorCodeFeature.Error = (long)errorCode; + KestrelMetrics.AddConnectionEndReason(_metricsContext, reason); + } + + public void Abort(ConnectionAbortedException ex, ConnectionEndReason reason) + { + Abort(ex, Http2ErrorCode.INTERNAL_ERROR, reason); + } + + public void Abort(ConnectionAbortedException ex, Http2ErrorCode errorCode, ConnectionEndReason reason) { if (TryClose()) { - _frameWriter.WriteGoAwayAsync(int.MaxValue, Http2ErrorCode.INTERNAL_ERROR).Preserve(); + SetConnectionErrorCode(reason, errorCode); + _frameWriter.WriteGoAwayAsync(int.MaxValue, errorCode).Preserve(); } _frameWriter.Abort(ex); } - public void StopProcessingNextRequest() - => StopProcessingNextRequest(serverInitiated: true); + public void StopProcessingNextRequest(ConnectionEndReason reason) + => StopProcessingNextRequest(serverInitiated: true, reason); public void HandleRequestHeadersTimeout() { Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); - Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout)); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.RequestHeadersTimeout); } public void HandleReadDataRateTimeout() @@ -233,15 +257,16 @@ public void HandleReadDataRateTimeout() Debug.Assert(Limits.MinRequestBodyDataRate != null); Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); - Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout)); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout), Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.MinRequestBodyDataRate); } - public void StopProcessingNextRequest(bool serverInitiated) + public void StopProcessingNextRequest(bool serverInitiated, ConnectionEndReason reason) { var initiator = serverInitiated ? GracefulCloseInitiator.Server : GracefulCloseInitiator.Client; if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None) { + _gracefulCloseReason = reason; Input.CancelPendingRead(); } } @@ -250,6 +275,7 @@ public void StopProcessingNextRequest(bool serverInitiated) { Exception? error = null; var errorCode = Http2ErrorCode.NO_ERROR; + var reason = ConnectionEndReason.Unset; try { @@ -260,6 +286,7 @@ public void StopProcessingNextRequest(bool serverInitiated) if (!await TryReadPrefaceAsync()) { + reason = ConnectionEndReason.TransportCompleted; return; } @@ -319,6 +346,7 @@ public void StopProcessingNextRequest(bool serverInitiated) if (result.IsCompleted) { + reason = ConnectionEndReason.TransportCompleted; return; } @@ -335,7 +363,7 @@ public void StopProcessingNextRequest(bool serverInitiated) { // There isn't a good error code to return with the GOAWAY. // NO_ERROR isn't a good choice because it indicates the connection is gracefully shutting down. - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorKeepAliveTimeout, Http2ErrorCode.INTERNAL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorKeepAliveTimeout, Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.KeepAliveTimeout); } } } @@ -353,6 +381,11 @@ public void StopProcessingNextRequest(bool serverInitiated) if (_clientActiveStreamCount > 0) { Log.RequestProcessingError(ConnectionId, ex); + reason = ConnectionEndReason.ConnectionReset; + } + else + { + reason = ConnectionEndReason.TransportCompleted; } error = ex; @@ -361,6 +394,7 @@ public void StopProcessingNextRequest(bool serverInitiated) { Log.RequestProcessingError(ConnectionId, ex); error = ex; + reason = ConnectionEndReason.IOError; } catch (ConnectionAbortedException ex) { @@ -372,6 +406,7 @@ public void StopProcessingNextRequest(bool serverInitiated) Log.Http2ConnectionError(ConnectionId, ex); error = ex; errorCode = ex.ErrorCode; + reason = ex.Reason; } catch (HPackDecodingException ex) { @@ -380,12 +415,14 @@ public void StopProcessingNextRequest(bool serverInitiated) Log.HPackDecodingError(ConnectionId, _currentHeadersStream.StreamId, ex); error = ex; errorCode = Http2ErrorCode.COMPRESSION_ERROR; + reason = ConnectionEndReason.ErrorReadingHeaders; } catch (Exception ex) { Log.LogWarning(0, ex, CoreStrings.RequestProcessingEndError); error = ex; errorCode = Http2ErrorCode.INTERNAL_ERROR; + reason = ConnectionEndReason.OtherError; } finally { @@ -396,6 +433,7 @@ public void StopProcessingNextRequest(bool serverInitiated) { if (TryClose()) { + SetConnectionErrorCode(reason, errorCode); await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode); } @@ -460,7 +498,7 @@ private void ValidateTlsRequirements() if (tlsFeature.Protocol < SslProtocols.Tls12) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorMinTlsVersion(tlsFeature.Protocol), Http2ErrorCode.INADEQUATE_SECURITY); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorMinTlsVersion(tlsFeature.Protocol), Http2ErrorCode.INADEQUATE_SECURITY, ConnectionEndReason.InsufficientTlsVersion); } } @@ -543,7 +581,10 @@ private async Task TryReadPrefaceAsync() await _context.Transport.Output.WriteAsync(responseBytes); // Close connection here so a GOAWAY frame isn't written. - TryClose(); + if (TryClose()) + { + SetConnectionErrorCode(ConnectionEndReason.InvalidHttpVersion, Http2ErrorCode.PROTOCOL_ERROR); + } return false; } @@ -557,7 +598,7 @@ private async Task TryReadPrefaceAsync() // Tested all states. Return HTTP/2 protocol error. if (state == ReadPrefaceState.None) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorInvalidPreface, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorInvalidPreface, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidHandshake); } } @@ -629,7 +670,7 @@ private static bool IsPreface(in ReadOnlySequence buffer, out SequencePosi // a connection error (Section 5.4.1) of type PROTOCOL_ERROR. if (_incomingFrame.StreamId != 0 && (_incomingFrame.StreamId & 1) == 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdEven(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdEven(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidStreamId); } return _incomingFrame.Type switch @@ -639,7 +680,7 @@ private static bool IsPreface(in ReadOnlySequence buffer, out SequencePosi Http2FrameType.PRIORITY => ProcessPriorityFrameAsync(), Http2FrameType.RST_STREAM => ProcessRstStreamFrameAsync(), Http2FrameType.SETTINGS => ProcessSettingsFrameAsync(payload), - Http2FrameType.PUSH_PROMISE => throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorPushPromiseReceived, Http2ErrorCode.PROTOCOL_ERROR), + Http2FrameType.PUSH_PROMISE => throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorPushPromiseReceived, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.UnsupportedFrame), Http2FrameType.PING => ProcessPingFrameAsync(payload), Http2FrameType.GOAWAY => ProcessGoAwayFrameAsync(), Http2FrameType.WINDOW_UPDATE => ProcessWindowUpdateFrameAsync(), @@ -652,17 +693,17 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId == 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdZeroException(); } if (_incomingFrame.DataHasPadding && _incomingFrame.DataPadLength >= _incomingFrame.PayloadLength) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidDataPadding); } ThrowIfIncomingFrameSentToIdleStream(); @@ -672,7 +713,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) if (stream.RstStreamReceived) { // Hard abort, do not allow any more frames on this stream. - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamAborted(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw CreateReceivedFrameStreamAbortedException(stream); } if (stream.EndStreamReceived) @@ -684,7 +725,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) // of type STREAM_CLOSED, unless the frame is permitted as described below. // // (The allowed frame types for this situation are WINDOW_UPDATE, RST_STREAM and PRIORITY) - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED, ConnectionEndReason.FrameAfterStreamClose); } return stream.OnDataAsync(_incomingFrame, payload); @@ -701,29 +742,55 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) // // We choose to do that here so we don't have to keep state to track implicitly closed // streams vs. streams closed with END_STREAM or RST_STREAM. - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED, ConnectionEndReason.UnknownStream); + } + + private Http2ConnectionErrorException CreateReceivedFrameStreamAbortedException(Http2Stream stream) + { + return new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamAborted(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED, ConnectionEndReason.FrameAfterStreamClose); + } + + private Http2ConnectionErrorException CreateStreamIdZeroException() + { + return new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidStreamId); + } + + private Http2ConnectionErrorException CreateStreamIdNotZeroException() + { + return new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidStreamId); + } + + private Http2ConnectionErrorException CreateHeadersInterleavedException() + { + Debug.Assert(_currentHeadersStream != null, "Only throw this error if parsing headers."); + return new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.UnexpectedFrame); + } + + private Http2ConnectionErrorException CreateUnexpectedFrameLengthException(int exceptedLength) + { + return new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, exceptedLength), Http2ErrorCode.FRAME_SIZE_ERROR, ConnectionEndReason.InvalidFrameLength); } private Task ProcessHeadersFrameAsync(IHttpApplication application, in ReadOnlySequence payload) where TContext : notnull { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId == 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdZeroException(); } if (_incomingFrame.HeadersHasPadding && _incomingFrame.HeadersPadLength >= _incomingFrame.PayloadLength - 1) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorPaddingTooLong(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidDataPadding); } if (_incomingFrame.HeadersHasPriority && _incomingFrame.HeadersStreamDependency == _incomingFrame.StreamId) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamSelfDependency(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamSelfDependency(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.StreamSelfDependency); } if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream)) @@ -731,7 +798,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) if (stream.RstStreamReceived) { // Hard abort, do not allow any more frames on this stream. - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamAborted(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw CreateReceivedFrameStreamAbortedException(stream); } // http://httpwg.org/specs/rfc7540.html#rfc.section.5.1 @@ -743,13 +810,13 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) // (The allowed frame types after END_STREAM are WINDOW_UPDATE, RST_STREAM and PRIORITY) if (stream.EndStreamReceived) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED, ConnectionEndReason.FrameAfterStreamClose); } // This is the last chance for the client to send END_STREAM if (!_incomingFrame.HeadersEndStream) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.MissingStreamEnd); } // Since we found an active stream, this HEADERS frame contains trailers @@ -768,7 +835,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) // // If we couldn't find the stream, it was previously closed (either implicitly or with // END_STREAM or RST_STREAM). - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED, ConnectionEndReason.InvalidStreamId); } else { @@ -836,22 +903,22 @@ private Task ProcessPriorityFrameAsync() { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId == 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdZeroException(); } if (_incomingFrame.PriorityStreamDependency == _incomingFrame.StreamId) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamSelfDependency(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamSelfDependency(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.StreamSelfDependency); } if (_incomingFrame.PayloadLength != 5) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 5), Http2ErrorCode.FRAME_SIZE_ERROR); + throw CreateUnexpectedFrameLengthException(exceptedLength: 5); } return Task.CompletedTask; @@ -861,17 +928,17 @@ private Task ProcessRstStreamFrameAsync() { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId == 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdZeroException(); } if (_incomingFrame.PayloadLength != 4) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 4), Http2ErrorCode.FRAME_SIZE_ERROR); + throw CreateUnexpectedFrameLengthException(exceptedLength: 4); } ThrowIfIncomingFrameSentToIdleStream(); @@ -901,19 +968,19 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence payload) { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId != 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdNotZeroException(); } if (_incomingFrame.SettingsAck) { if (_incomingFrame.PayloadLength != 0) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsAckLengthNotZero, Http2ErrorCode.FRAME_SIZE_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsAckLengthNotZero, Http2ErrorCode.FRAME_SIZE_ERROR, ConnectionEndReason.InvalidFrameLength); } return Task.CompletedTask; @@ -921,7 +988,7 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence payload) if (_incomingFrame.PayloadLength % 6 != 0) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsLengthNotMultipleOfSix, Http2ErrorCode.FRAME_SIZE_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorSettingsLengthNotMultipleOfSix, Http2ErrorCode.FRAME_SIZE_ERROR, ConnectionEndReason.InvalidFrameLength); } try @@ -955,7 +1022,7 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence payload) // This means that this caused a stream window to become larger than int.MaxValue. // This can never happen with a well behaved client and MUST be treated as a connection error. // https://httpwg.org/specs/rfc7540.html#rfc.section.6.9.2 - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorInitialWindowSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorInitialWindowSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR, ConnectionEndReason.InvalidSettings); } } } @@ -973,9 +1040,11 @@ private Task ProcessSettingsFrameAsync(in ReadOnlySequence payload) } catch (Http2SettingsParameterOutOfRangeException ex) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorSettingsParameterOutOfRange(ex.Parameter), ex.Parameter == Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE + var errorCode = ex.Parameter == Http2SettingsParameter.SETTINGS_INITIAL_WINDOW_SIZE ? Http2ErrorCode.FLOW_CONTROL_ERROR - : Http2ErrorCode.PROTOCOL_ERROR); + : Http2ErrorCode.PROTOCOL_ERROR; + + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorSettingsParameterOutOfRange(ex.Parameter), errorCode, ConnectionEndReason.InvalidSettings); } } @@ -983,17 +1052,17 @@ private Task ProcessPingFrameAsync(in ReadOnlySequence payload) { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId != 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdNotZeroException(); } if (_incomingFrame.PayloadLength != 8) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 8), Http2ErrorCode.FRAME_SIZE_ERROR); + throw CreateUnexpectedFrameLengthException(exceptedLength: 8); } // Incoming ping resets connection keep alive timeout @@ -1015,16 +1084,16 @@ private Task ProcessGoAwayFrameAsync() { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.StreamId != 0) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdNotZero(_incomingFrame.Type), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateStreamIdNotZeroException(); } // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated. - StopProcessingNextRequest(serverInitiated: false); + StopProcessingNextRequest(serverInitiated: false, ConnectionEndReason.ClientGoAway); _context.ConnectionFeatures.Get()?.RequestClose(); return Task.CompletedTask; @@ -1034,12 +1103,12 @@ private Task ProcessWindowUpdateFrameAsync() { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_incomingFrame.PayloadLength != 4) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, 4), Http2ErrorCode.FRAME_SIZE_ERROR); + throw CreateUnexpectedFrameLengthException(exceptedLength: 4); } ThrowIfIncomingFrameSentToIdleStream(); @@ -1061,14 +1130,14 @@ private Task ProcessWindowUpdateFrameAsync() // Since server initiated stream resets are not yet properly // implemented and tested, we treat all zero length window // increments as connection errors for now. - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateIncrementZero, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateIncrementZero, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.WindowUpdateSizeInvalid); } if (_incomingFrame.StreamId == 0) { if (!_frameWriter.TryUpdateConnectionWindow(_incomingFrame.WindowUpdateSizeIncrement)) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR, ConnectionEndReason.WindowUpdateSizeInvalid); } } else if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream)) @@ -1076,7 +1145,7 @@ private Task ProcessWindowUpdateFrameAsync() if (stream.RstStreamReceived) { // Hard abort, do not allow any more frames on this stream. - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamAborted(_incomingFrame.Type, stream.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw CreateReceivedFrameStreamAbortedException(stream); } if (!stream.TryUpdateOutputWindow(_incomingFrame.WindowUpdateSizeIncrement)) @@ -1098,12 +1167,12 @@ private Task ProcessContinuationFrameAsync(in ReadOnlySequence payload) { if (_currentHeadersStream == null) { - throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorContinuationWithNoHeaders, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorContinuationWithNoHeaders, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.UnexpectedFrame); } if (_incomingFrame.StreamId != _currentHeadersStream.StreamId) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) @@ -1127,7 +1196,7 @@ private Task ProcessUnknownFrameAsync() { if (_currentHeadersStream != null) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorHeadersInterleaved(_incomingFrame.Type, _incomingFrame.StreamId, _currentHeadersStream.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw CreateHeadersInterleavedException(); } return Task.CompletedTask; @@ -1236,9 +1305,9 @@ private void StartStream() // messages in case they somehow make it back to the client (not expected) // This will close the socket - we want to do that right away - Abort(new ConnectionAbortedException(CoreStrings.Http2ConnectionFaulted)); + Abort(new ConnectionAbortedException(CoreStrings.Http2ConnectionFaulted), Http2ErrorCode.ENHANCE_YOUR_CALM, ConnectionEndReason.StreamResetLimitExceeded); // Throwing an exception as well will help us clean up on our end more quickly by (e.g.) skipping processing of already-buffered input - throw new Http2ConnectionErrorException(CoreStrings.Http2ConnectionFaulted, Http2ErrorCode.ENHANCE_YOUR_CALM); + throw new Http2ConnectionErrorException(CoreStrings.Http2ConnectionFaulted, Http2ErrorCode.ENHANCE_YOUR_CALM, ConnectionEndReason.StreamResetLimitExceeded); } throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2TellClientToCalmDown, Http2ErrorCode.ENHANCE_YOUR_CALM); @@ -1297,7 +1366,7 @@ private void ThrowIfIncomingFrameSentToIdleStream() // initial state for all streams. if (_incomingFrame.StreamId > _highestOpenedStreamId) { - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdle(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamIdle(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidStreamId); } } @@ -1353,7 +1422,7 @@ private void UpdateCompletedStreams() if (stream == _currentHeadersStream) { // The drain expired out while receiving trailers. The most recent incoming frame is either a header or continuation frame for the timed out stream. - throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED); + throw new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorStreamClosed(_incomingFrame.Type, _incomingFrame.StreamId), Http2ErrorCode.STREAM_CLOSED, ConnectionEndReason.FrameAfterStreamClose); } RemoveStream(stream); @@ -1431,6 +1500,7 @@ private void UpdateConnectionState() { if (TryClose()) { + SetConnectionErrorCode(_gracefulCloseReason, Http2ErrorCode.NO_ERROR); _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.NO_ERROR).Preserve(); } } @@ -1496,7 +1566,7 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly // Allow a 2x grace before aborting the connection. We'll check the size limit again later where we can send a 431. if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize * 2) { - throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } try @@ -1557,11 +1627,11 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly } catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre) { - throw new Http2ConnectionErrorException(bre.Message, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(bre.Message, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } catch (InvalidOperationException) { - throw new Http2ConnectionErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } } @@ -1572,7 +1642,7 @@ private void ValidateHeaderContent(ReadOnlySpan name, ReadOnlySpan v { if (IsConnectionSpecificHeaderField(name, value)) { - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2 @@ -1583,11 +1653,11 @@ private void ValidateHeaderContent(ReadOnlySpan name, ReadOnlySpan v { if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } else { - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } } } @@ -1616,13 +1686,13 @@ private void UpdateHeaderParsingState(ReadOnlySpan value, PseudoHeaderFiel // All pseudo-header fields MUST appear in the header block before regular header fields. // Any request or response that contains a pseudo-header field that appears in a header // block after a regular header field MUST be treated as malformed (Section 8.1.2.6). - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { // Pseudo-header fields MUST NOT appear in trailers. - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } _requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields; @@ -1631,21 +1701,21 @@ private void UpdateHeaderParsingState(ReadOnlySpan value, PseudoHeaderFiel { // Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header // fields as malformed (Section 8.1.2.6). - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorUnknownPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorUnknownPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } if (headerField == PseudoHeaderFields.Status) { // Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields // defined for responses MUST NOT appear in requests. - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorResponsePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorResponsePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } if ((_parsedPseudoHeaderFields & headerField) == headerField) { // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3 // All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields - throw new Http2ConnectionErrorException(CoreStrings.HttpErrorDuplicatePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR); + throw new Http2ConnectionErrorException(CoreStrings.HttpErrorDuplicatePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.InvalidRequestHeaders); } if (headerField == PseudoHeaderFields.Method) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs index b799ef02797c..2854a6aa7b68 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2FrameWriter.cs @@ -154,7 +154,7 @@ public void Schedule(Http2OutputProducer producer) // exceeding the channel size. Disconnecting seems appropriate in this case. var ex = new ConnectionAbortedException("HTTP/2 connection exceeded the output operations maximum queue size."); _log.Http2QueueOperationsExceeded(_connectionId, ex); - _http2Connection.Abort(ex); + _http2Connection.Abort(ex, Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.OutputQueueSizeExceeded); } } @@ -323,14 +323,17 @@ static bool HasStateFlag(Http2OutputProducer.State state, Http2OutputProducer.St private async Task HandleFlowControlErrorAsync() { - var connectionError = new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, Http2ErrorCode.FLOW_CONTROL_ERROR); + const ConnectionEndReason reason = ConnectionEndReason.WindowUpdateSizeInvalid; + const Http2ErrorCode http2ErrorCode = Http2ErrorCode.FLOW_CONTROL_ERROR; + + var connectionError = new Http2ConnectionErrorException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, http2ErrorCode, reason); _log.Http2ConnectionError(_connectionId, connectionError); - await WriteGoAwayAsync(int.MaxValue, Http2ErrorCode.FLOW_CONTROL_ERROR); + await WriteGoAwayAsync(int.MaxValue, http2ErrorCode); // Prevent Abort() from writing an INTERNAL_ERROR GOAWAY frame after our FLOW_CONTROL_ERROR. Complete(); // Stop processing any more requests and immediately close the connection. - _http2Connection.Abort(new ConnectionAbortedException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, connectionError)); + _http2Connection.Abort(new ConnectionAbortedException(CoreStrings.Http2ErrorWindowUpdateSizeInvalid, connectionError), http2ErrorCode, reason); } private bool TryQueueProducerForConnectionWindowUpdate(long actual, Http2OutputProducer producer) @@ -527,7 +530,7 @@ private void WriteResponseHeadersUnsynchronized(int streamId, int statusCode, Ht catch (Exception ex) { _log.HPackEncodingError(_connectionId, streamId, ex); - _http2Connection.Abort(new ConnectionAbortedException(ex.Message, ex)); + _http2Connection.Abort(new ConnectionAbortedException(ex.Message, ex), Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.ErrorWritingHeaders); } } @@ -568,7 +571,7 @@ private ValueTask WriteDataAndTrailersAsync(Http2Stream stream, in catch (Exception ex) { _log.HPackEncodingError(_connectionId, streamId, ex); - _http2Connection.Abort(new ConnectionAbortedException(ex.Message, ex)); + _http2Connection.Abort(new ConnectionAbortedException(ex.Message, ex), Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.ErrorWritingHeaders); } return TimeFlushUnsynchronizedAsync(); @@ -1099,7 +1102,7 @@ private void EnqueueWaitingForMoreConnectionWindow(Http2OutputProducer producer) if (!_aborted && IsFlowControlQueueLimitEnabled && _waitingForMoreConnectionWindow.Count > _maximumFlowControlQueueSize) { _log.Http2FlowControlQueueOperationsExceeded(_connectionId, _maximumFlowControlQueueSize); - _http2Connection.Abort(new ConnectionAbortedException("HTTP/2 connection exceeded the outgoing flow control maximum queue size.")); + _http2Connection.Abort(new ConnectionAbortedException("HTTP/2 connection exceeded the outgoing flow control maximum queue size."), Http2ErrorCode.INTERNAL_ERROR, ConnectionEndReason.FlowControlQueueSizeExceeded); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs index f65890adf7cb..4f7b8606590f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -241,7 +241,7 @@ public void Complete() // This is called when a CancellationToken fires mid-write. In HTTP/1.x, this aborts the entire connection. // For HTTP/2 we abort the stream. - void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason) + void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason, ConnectionEndReason reason) { _stream.ResetAndAbort(abortReason, Http2ErrorCode.INTERNAL_ERROR); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs index 9a53fddf98e3..e0f300839104 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Connection.cs @@ -45,6 +45,7 @@ internal sealed class Http3Connection : IHttp3StreamLifetimeHandler, IRequestPro private long _highestOpenedRequestStreamId = DefaultHighestOpenedRequestStreamId; private bool _aborted; private int _gracefulCloseInitiator; + private ConnectionEndReason _gracefulCloseReason; private int _stoppedAcceptingStreams; private bool _gracefulCloseStarted; private int _activeRequestCount; @@ -55,8 +56,7 @@ public Http3Connection(HttpMultiplexedConnectionContext context) _multiplexedContext = (MultiplexedConnectionContext)context.ConnectionContext; _context = context; _streamLifetimeHandler = this; - MetricsContext = context.ConnectionFeatures.GetRequiredFeature().MetricsContext; - + MetricsContext = context.MetricsContext; _errorCodeFeature = context.ConnectionFeatures.GetRequiredFeature(); var httpLimits = context.ServiceContext.ServerOptions.Limits; @@ -101,10 +101,10 @@ private void UpdateHighestOpenedRequestStreamId(long streamId) public string ConnectionId => _context.ConnectionId; public ITimeoutControl TimeoutControl => _context.TimeoutControl; - public void StopProcessingNextRequest() - => StopProcessingNextRequest(serverInitiated: true); + public void StopProcessingNextRequest(ConnectionEndReason reason) + => StopProcessingNextRequest(serverInitiated: true, reason); - public void StopProcessingNextRequest(bool serverInitiated) + public void StopProcessingNextRequest(bool serverInitiated, ConnectionEndReason reason) { bool previousState; lock (_protocolSelectionLock) @@ -118,6 +118,8 @@ public void StopProcessingNextRequest(bool serverInitiated) if (Interlocked.CompareExchange(ref _gracefulCloseInitiator, initiator, GracefulCloseInitiator.None) == GracefulCloseInitiator.None) { + _gracefulCloseReason = reason; + // Break out of AcceptStreams so connection state can be updated. _acceptStreamsCts.Cancel(); } @@ -149,12 +151,12 @@ private bool TryStopAcceptingStreams() return false; } - public void Abort(ConnectionAbortedException ex) + public void Abort(ConnectionAbortedException ex, ConnectionEndReason reason) { - Abort(ex, Http3ErrorCode.InternalError); + Abort(ex, Http3ErrorCode.InternalError, reason); } - public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) + public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode, ConnectionEndReason reason) { bool previousState; @@ -182,6 +184,7 @@ public void Abort(ConnectionAbortedException ex, Http3ErrorCode errorCode) if (!previousState) { _errorCodeFeature.Error = (long)errorCode; + KestrelMetrics.AddConnectionEndReason(MetricsContext, reason); if (TryStopAcceptingStreams()) { @@ -235,7 +238,7 @@ static void ValidateOpenControlStream(Http3ControlStream? stream, Http3Connectio if (stream.StreamTimeoutTimestamp < timestamp) { - connection.OnStreamConnectionError(new Http3ConnectionErrorException("A control stream used by the connection was closed or reset.", Http3ErrorCode.ClosedCriticalStream)); + connection.OnStreamConnectionError(new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamClosed, Http3ErrorCode.ClosedCriticalStream, ConnectionEndReason.ClosedCriticalStream)); } } } @@ -313,7 +316,7 @@ private void UpdateStreamTimeouts(long timestamp) { // Cancel connection to be consistent with other data rate limits. Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, stream.TraceIdentifier); - OnStreamConnectionError(new Http3ConnectionErrorException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied, Http3ErrorCode.InternalError)); + OnStreamConnectionError(new Http3ConnectionErrorException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied, Http3ErrorCode.InternalError, ConnectionEndReason.MinResponseDataRate)); } } } @@ -336,6 +339,7 @@ private void UpdateStreamTimeouts(long timestamp) Http3ControlStream? outboundControlStream = null; ValueTask outboundControlStreamTask = default; bool clientAbort = false; + ConnectionEndReason reason = ConnectionEndReason.Unset; try { @@ -459,6 +463,7 @@ private void UpdateStreamTimeouts(long timestamp) if (_activeRequestCount > 0) { Log.RequestProcessingError(_context.ConnectionId, ex); + reason = ConnectionEndReason.ConnectionReset; } } error = ex; @@ -468,20 +473,24 @@ private void UpdateStreamTimeouts(long timestamp) { Log.RequestProcessingError(_context.ConnectionId, ex); error = ex; + reason = ConnectionEndReason.IOError; } catch (ConnectionAbortedException ex) { Log.RequestProcessingError(_context.ConnectionId, ex); error = ex; + reason = ConnectionEndReason.OtherError; } catch (Http3ConnectionErrorException ex) { Log.Http3ConnectionError(_context.ConnectionId, ex); error = ex; + reason = ex.Reason; } catch (Exception ex) { error = ex; + reason = ConnectionEndReason.OtherError; } finally { @@ -530,8 +539,14 @@ private void UpdateStreamTimeouts(long timestamp) await outboundControlStreamTask; } + // Use graceful close reason if it has been set. + if (reason == ConnectionEndReason.Unset && _gracefulCloseReason != ConnectionEndReason.Unset) + { + reason = _gracefulCloseReason; + } + // Complete - Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error); + Abort(CreateConnectionAbortError(error, clientAbort), (Http3ErrorCode)_errorCodeFeature.Error, reason); // Wait for active requests to complete. while (_activeRequestCount > 0) @@ -543,7 +558,7 @@ private void UpdateStreamTimeouts(long timestamp) } catch { - Abort(CreateConnectionAbortError(error, clientAbort), Http3ErrorCode.InternalError); + Abort(CreateConnectionAbortError(error, clientAbort), Http3ErrorCode.InternalError, ConnectionEndReason.OtherError); throw; } finally @@ -704,11 +719,11 @@ private async ValueTask ProcessOutboundControlStreamAsync(Http3ControlStream con { Log.Http3OutboundControlStreamError(ConnectionId, ex); - var connectionError = new Http3ConnectionErrorException(CoreStrings.Http3ControlStreamErrorInitializingOutbound, Http3ErrorCode.ClosedCriticalStream); + var connectionError = new Http3ConnectionErrorException(CoreStrings.Http3ControlStreamErrorInitializingOutbound, Http3ErrorCode.ClosedCriticalStream, ConnectionEndReason.ClosedCriticalStream); Log.Http3ConnectionError(ConnectionId, connectionError); // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1 - Abort(new ConnectionAbortedException(connectionError.Message, connectionError), connectionError.ErrorCode); + Abort(new ConnectionAbortedException(connectionError.Message, connectionError), connectionError.ErrorCode, ConnectionEndReason.ClosedCriticalStream); } } @@ -841,7 +856,7 @@ void IHttp3StreamLifetimeHandler.OnStreamConnectionError(Http3ConnectionErrorExc private void OnStreamConnectionError(Http3ConnectionErrorException ex) { Log.Http3ConnectionError(ConnectionId, ex); - Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode); + Abort(new ConnectionAbortedException(ex.Message, ex), ex.ErrorCode, ex.Reason); } void IHttp3StreamLifetimeHandler.OnInboundControlStreamSetting(Http3SettingType type, long value) @@ -874,7 +889,7 @@ void IHttp3StreamLifetimeHandler.OnStreamHeaderReceived(IHttp3Stream stream) public void HandleRequestHeadersTimeout() { Log.ConnectionBadRequest(ConnectionId, KestrelBadHttpRequestException.GetException(RequestRejectionReason.RequestHeadersTimeout)); - Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout)); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestHeadersTimeout), ConnectionEndReason.RequestHeadersTimeout); } public void HandleReadDataRateTimeout() @@ -882,7 +897,7 @@ public void HandleReadDataRateTimeout() Debug.Assert(Limits.MinRequestBodyDataRate != null); Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, null, Limits.MinRequestBodyDataRate.BytesPerSecond); - Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout)); + Abort(new ConnectionAbortedException(CoreStrings.BadRequest_RequestBodyTimeout), ConnectionEndReason.MinRequestBodyDataRate); } public void OnInputOrOutputCompleted() @@ -890,7 +905,7 @@ public void OnInputOrOutputCompleted() TryStopAcceptingStreams(); // Abort the connection using the error code the client used. For a graceful close, this should be H3_NO_ERROR. - Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), (Http3ErrorCode)_errorCodeFeature.Error); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), (Http3ErrorCode)_errorCodeFeature.Error, ConnectionEndReason.TransportCompleted); } internal WebTransportSession OpenNewWebTransportSession(Http3Stream http3Stream) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs index 14ad5d095643..7140fe795b9a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ConnectionErrorException.cs @@ -7,11 +7,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; internal sealed class Http3ConnectionErrorException : Exception { - public Http3ConnectionErrorException(string message, Http3ErrorCode errorCode) + public Http3ConnectionErrorException(string message, Http3ErrorCode errorCode, ConnectionEndReason reason) : base($"HTTP/3 connection error ({Http3Formatting.ToFormattedErrorCode(errorCode)}): {message}") { ErrorCode = errorCode; + Reason = reason; } public Http3ErrorCode ErrorCode { get; } + public ConnectionEndReason Reason { get; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs index 599a55f50212..dbd99d838a0e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3ControlStream.cs @@ -202,7 +202,7 @@ private async ValueTask TryReadStreamHeaderAsync() if (!_context.StreamLifetimeHandler.OnInboundControlStream(this)) { // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-6.2.1 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError); + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("control"), Http3ErrorCode.StreamCreationError, ConnectionEndReason.StreamCreationError); } await HandleControlStream(); @@ -211,7 +211,7 @@ private async ValueTask TryReadStreamHeaderAsync() if (!_context.StreamLifetimeHandler.OnInboundEncoderStream(this)) { // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError); + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("encoder"), Http3ErrorCode.StreamCreationError, ConnectionEndReason.StreamCreationError); } await HandleEncodingDecodingTask(); @@ -220,7 +220,7 @@ private async ValueTask TryReadStreamHeaderAsync() if (!_context.StreamLifetimeHandler.OnInboundDecoderStream(this)) { // https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#section-4.2 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError); + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams("decoder"), Http3ErrorCode.StreamCreationError, ConnectionEndReason.StreamCreationError); } await HandleEncodingDecodingTask(); break; @@ -302,7 +302,7 @@ private ValueTask ProcessHttp3ControlStream(in ReadOnlySequence payload) case Http3FrameType.Headers: case Http3FrameType.PushPromise: // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2 - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); case Http3FrameType.Settings: return ProcessSettingsFrameAsync(payload); case Http3FrameType.GoAway: @@ -321,7 +321,7 @@ private ValueTask ProcessSettingsFrameAsync(ReadOnlySequence payload) if (_haveReceivedSettingsFrame) { // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-settings - throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(CoreStrings.Http3ErrorControlStreamMultipleSettingsFrames, Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); } _haveReceivedSettingsFrame = true; @@ -367,7 +367,7 @@ private void ProcessSetting(long id, long value) // HTTP/2 settings are reserved. // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-7.2.4.1-5 var message = CoreStrings.FormatHttp3ErrorControlStreamReservedSetting("0x" + id.ToString("X", CultureInfo.InvariantCulture)); - throw new Http3ConnectionErrorException(message, Http3ErrorCode.SettingsError); + throw new Http3ConnectionErrorException(message, Http3ErrorCode.SettingsError, ConnectionEndReason.InvalidSettings); case (long)Http3SettingType.QPackMaxTableCapacity: case (long)Http3SettingType.MaxFieldSectionSize: case (long)Http3SettingType.QPackBlockedStreams: @@ -387,7 +387,7 @@ private ValueTask ProcessGoAwayFrameAsync() EnsureSettingsFrame(Http3FrameType.GoAway); // StopProcessingNextRequest must be called before RequestClose to ensure it's considered client initiated. - _context.Connection.StopProcessingNextRequest(serverInitiated: false); + _context.Connection.StopProcessingNextRequest(serverInitiated: false, ConnectionEndReason.ClientGoAway); _context.ConnectionContext.Features.Get()?.RequestClose(); // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-goaway @@ -431,7 +431,7 @@ private void EnsureSettingsFrame(Http3FrameType frameType) if (!_haveReceivedSettingsFrame) { var message = CoreStrings.FormatHttp3ErrorControlStreamFrameReceivedBeforeSettings(Http3Formatting.ToFormattedType(frameType)); - throw new Http3ConnectionErrorException(message, Http3ErrorCode.MissingSettings); + throw new Http3ConnectionErrorException(message, Http3ErrorCode.MissingSettings, ConnectionEndReason.InvalidSettings); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs index d387d3629662..e8e69ec40e85 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs @@ -95,7 +95,7 @@ public void Dispose() } } - void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason) + void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason, ConnectionEndReason reason) { _stream.Abort(abortReason, Http3ErrorCode.InternalError); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs index 3ce5cdcad632..823be2934dfb 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -757,10 +757,10 @@ Http3FrameType.Settings or Http3FrameType.CancelPush or Http3FrameType.GoAway or Http3FrameType.MaxPushId => throw new Http3ConnectionErrorException( - CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame), + CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnsupportedFrame), // The server should never receive push promise Http3FrameType.PushPromise => throw new Http3ConnectionErrorException( - CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame), + CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(_incomingFrame.FormattedType), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnsupportedFrame), _ => ProcessUnknownFrameAsync(), }; } @@ -778,7 +778,7 @@ private static Task ProcessUnknownFrameAsync() // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1 if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { - throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Headers)), Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Headers)), Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); } if (_requestHeaderParsingState == RequestHeaderParsingState.Body) @@ -877,7 +877,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1 if (_requestHeaderParsingState == RequestHeaderParsingState.Ready) { - throw new Http3ConnectionErrorException(CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders, Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(CoreStrings.Http3StreamErrorDataReceivedBeforeHeaders, Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); } // DATA frame after trailing headers is invalid. @@ -885,7 +885,7 @@ private Task ProcessDataFrameAsync(in ReadOnlySequence payload) if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers) { var message = CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data)); - throw new Http3ConnectionErrorException(message, Http3ErrorCode.UnexpectedFrame); + throw new Http3ConnectionErrorException(message, Http3ErrorCode.UnexpectedFrame, ConnectionEndReason.UnexpectedFrame); } if (InputRemaining.HasValue) diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs index feab55224929..d65c39876450 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnection.cs @@ -45,8 +45,12 @@ public HttpConnection(BaseHttpConnectionContext context) public async Task ProcessRequestsAsync(IHttpApplication httpApplication) where TContext : notnull { + IConnectionMetricsTagsFeature? connectionMetricsTagsFeature = null; + try { + connectionMetricsTagsFeature = _context.ConnectionFeatures.Get(); + // Ensure TimeoutControl._lastTimestamp is initialized before anything that could set timeouts runs. _timeoutControl.Initialize(); @@ -64,6 +68,7 @@ public HttpConnection(BaseHttpConnectionContext context) // _http2Connection must be initialized before yielding control to the transport thread, // to prevent a race condition where _http2Connection.Abort() is called just as // _http2Connection is about to be initialized. + _context.ConnectionFeatures.Set(new ProtocolErrorCodeFeature()); requestProcessor = new Http2Connection((HttpConnectionContext)_context); _protocolSelectionState = ProtocolSelectionState.Selected; AddMetricsHttpProtocolTag(KestrelMetrics.Http2); @@ -100,7 +105,7 @@ public HttpConnection(BaseHttpConnectionContext context) connectionHeartbeatFeature?.OnHeartbeat(state => ((HttpConnection)state).Tick(), this); // Register for graceful shutdown of the server - using var shutdownRegistration = connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((HttpConnection)state!).StopProcessingNextRequest(), this); + using var shutdownRegistration = connectionLifetimeNotificationFeature?.ConnectionClosedRequested.Register(state => ((HttpConnection)state!).StopProcessingNextRequest(ConnectionEndReason.GracefulAppShutdown), this); // Register for connection close using var closedRegistration = _context.ConnectionContext.ConnectionClosed.Register(state => ((HttpConnection)state!).OnConnectionClosed(), this); @@ -112,6 +117,18 @@ public HttpConnection(BaseHttpConnectionContext context) { Log.LogCritical(0, ex, $"Unexpected exception in {nameof(HttpConnection)}.{nameof(ProcessRequestsAsync)}."); } + finally + { + if (_context.MetricsContext.ConnectionEndReason is { } connectionEndReason) + { + KestrelMetrics.AddConnectionEndReason(connectionMetricsTagsFeature, connectionEndReason); + } + } + } + + private sealed class ProtocolErrorCodeFeature : IProtocolErrorCodeFeature + { + public long Error { get; set; } = -1; } private void AddMetricsHttpProtocolTag(string httpVersion) @@ -131,7 +148,7 @@ internal void Initialize(IRequestProcessor requestProcessor) _protocolSelectionState = ProtocolSelectionState.Selected; } - private void StopProcessingNextRequest() + private void StopProcessingNextRequest(ConnectionEndReason reason) { ProtocolSelectionState previousState; lock (_protocolSelectionLock) @@ -143,7 +160,7 @@ private void StopProcessingNextRequest() switch (previousState) { case ProtocolSelectionState.Selected: - _requestProcessor!.StopProcessingNextRequest(); + _requestProcessor!.StopProcessingNextRequest(reason); break; case ProtocolSelectionState.Aborted: break; @@ -169,7 +186,7 @@ private void OnConnectionClosed() } } - private void Abort(ConnectionAbortedException ex) + private void Abort(ConnectionAbortedException ex, ConnectionEndReason reason) { ProtocolSelectionState previousState; @@ -184,7 +201,7 @@ private void Abort(ConnectionAbortedException ex) switch (previousState) { case ProtocolSelectionState.Selected: - _requestProcessor!.Abort(ex); + _requestProcessor!.Abort(ex, reason); break; case ProtocolSelectionState.Aborted: break; @@ -259,7 +276,7 @@ public void OnTimeout(TimeoutReason reason) switch (reason) { case TimeoutReason.KeepAlive: - _requestProcessor!.StopProcessingNextRequest(); + _requestProcessor!.StopProcessingNextRequest(ConnectionEndReason.KeepAliveTimeout); break; case TimeoutReason.RequestHeaders: _requestProcessor!.HandleRequestHeadersTimeout(); @@ -269,11 +286,11 @@ public void OnTimeout(TimeoutReason reason) break; case TimeoutReason.WriteDataRate: Log.ResponseMinimumDataRateNotSatisfied(_context.ConnectionId, _http1Connection?.TraceIdentifier); - Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied), ConnectionEndReason.MinResponseDataRate); break; case TimeoutReason.RequestBodyDrain: case TimeoutReason.TimeoutFeature: - Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer)); + Abort(new ConnectionAbortedException(CoreStrings.ConnectionTimedOutByServer), ConnectionEndReason.ServerTimeout); break; default: Debug.Assert(false, "Invalid TimeoutReason"); diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs index 92c6ad1a0583..98afcead381a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpConnectionContext.cs @@ -22,11 +22,9 @@ internal class HttpConnectionContext : BaseHttpConnectionContext MemoryPool memoryPool, IPEndPoint? localEndPoint, IPEndPoint? remoteEndPoint, - ConnectionMetricsContext metricsContext) : base(connectionId, protocols, altSvcHeader, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint) + ConnectionMetricsContext metricsContext) : base(connectionId, protocols, altSvcHeader, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint, metricsContext) { - MetricsContext = metricsContext; } public IDuplexPipe Transport { get; set; } = default!; - public ConnectionMetricsContext MetricsContext { get; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs b/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs index a579e0e8e3e1..9bb3df1850a6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/HttpMultiplexedConnectionContext.cs @@ -20,7 +20,8 @@ internal sealed class HttpMultiplexedConnectionContext : BaseHttpConnectionConte IFeatureCollection connectionFeatures, MemoryPool memoryPool, IPEndPoint? localEndPoint, - IPEndPoint? remoteEndPoint) : base(connectionId, protocols, altSvcHeader, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint) + IPEndPoint? remoteEndPoint, + ConnectionMetricsContext metricsContext) : base(connectionId, protocols, altSvcHeader, connectionContext, serviceContext, connectionFeatures, memoryPool, localEndPoint, remoteEndPoint, metricsContext) { } } diff --git a/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs b/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs index b1b5cfddbb14..8f314ffca1e5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs +++ b/src/Servers/Kestrel/Core/src/Internal/IRequestProcessor.cs @@ -9,10 +9,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; internal interface IRequestProcessor { Task ProcessRequestsAsync(IHttpApplication application) where TContext : notnull; - void StopProcessingNextRequest(); + void StopProcessingNextRequest(ConnectionEndReason reason); void HandleRequestHeadersTimeout(); void HandleReadDataRateTimeout(); void OnInputOrOutputCompleted(); void Tick(long timestamp); - void Abort(ConnectionAbortedException ex); + void Abort(ConnectionAbortedException ex, ConnectionEndReason reason); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionMetricsContext.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionMetricsContext.cs index f561bda2437e..e5f6e534ffc3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionMetricsContext.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/ConnectionMetricsContext.cs @@ -1,30 +1,19 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Connections; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; -internal readonly struct ConnectionMetricsContext +internal sealed class ConnectionMetricsContext { - public BaseConnectionContext ConnectionContext { get; } - public bool CurrentConnectionsCounterEnabled { get; } - public bool ConnectionDurationEnabled { get; } - public bool QueuedConnectionsCounterEnabled { get; } - public bool QueuedRequestsCounterEnabled { get; } - public bool CurrentUpgradedRequestsCounterEnabled { get; } - public bool CurrentTlsHandshakesCounterEnabled { get; } + public required BaseConnectionContext ConnectionContext { get; init; } + public bool CurrentConnectionsCounterEnabled { get; init; } + public bool ConnectionDurationEnabled { get; init; } + public bool QueuedConnectionsCounterEnabled { get; init; } + public bool QueuedRequestsCounterEnabled { get; init; } + public bool CurrentUpgradedRequestsCounterEnabled { get; init; } + public bool CurrentTlsHandshakesCounterEnabled { get; init; } - public ConnectionMetricsContext(BaseConnectionContext connectionContext, bool currentConnectionsCounterEnabled, - bool connectionDurationEnabled, bool queuedConnectionsCounterEnabled, bool queuedRequestsCounterEnabled, - bool currentUpgradedRequestsCounterEnabled, bool currentTlsHandshakesCounterEnabled) - { - ConnectionContext = connectionContext; - CurrentConnectionsCounterEnabled = currentConnectionsCounterEnabled; - ConnectionDurationEnabled = connectionDurationEnabled; - QueuedConnectionsCounterEnabled = queuedConnectionsCounterEnabled; - QueuedRequestsCounterEnabled = queuedRequestsCounterEnabled; - CurrentUpgradedRequestsCounterEnabled = currentUpgradedRequestsCounterEnabled; - CurrentTlsHandshakesCounterEnabled = currentTlsHandshakesCounterEnabled; - } + public ConnectionEndReason? ConnectionEndReason { get; set; } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IConnectionMetricsContextFeature.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IConnectionMetricsContextFeature.cs index a1266549de3c..f9dc25b5f193 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IConnectionMetricsContextFeature.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/IConnectionMetricsContextFeature.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs index 10768d162a21..46c6da2f26cf 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelMetrics.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Security.Authentication; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -18,6 +19,8 @@ internal sealed class KestrelMetrics // Note: Dot separated instead of dash. public const string MeterName = "Microsoft.AspNetCore.Server.Kestrel"; + public const string ErrorType = "error.type"; + public const string Http11 = "1.1"; public const string Http2 = "2"; public const string Http3 = "3"; @@ -99,12 +102,18 @@ public void ConnectionStop(in ConnectionMetricsContext metricsContext, Exception { if (metricsContext.CurrentConnectionsCounterEnabled || metricsContext.ConnectionDurationEnabled) { - ConnectionStopCore(metricsContext, exception, customTags, startTimestamp, currentTimestamp); + // Add protocol error code if feature is available and it's not the unset value (-1). + long? errorCode = null; + if (metricsContext.ConnectionContext.Features.Get() is IProtocolErrorCodeFeature errorCodeFeature && errorCodeFeature.Error != -1) + { + errorCode = errorCodeFeature.Error; + } + ConnectionStopCore(metricsContext, exception, errorCode, customTags, startTimestamp, currentTimestamp); } } [MethodImpl(MethodImplOptions.NoInlining)] - private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) + private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exception? exception, long? protocolErrorCode, List>? customTags, long startTimestamp, long currentTimestamp) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); @@ -117,9 +126,19 @@ private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exce if (metricsContext.ConnectionDurationEnabled) { - if (exception != null) + if (protocolErrorCode != null) { - tags.Add("error.type", exception.GetType().FullName); + tags.Add("http.connection.protocol_code", protocolErrorCode); + } + + // Check if there is an end reason on the context. For example, the connection could have been aborted by shutdown. + if (metricsContext.ConnectionEndReason is { } reason && TryGetErrorType(reason, out var errorValue)) + { + tags.TryAddTag(ErrorType, errorValue); + } + else if (exception != null) + { + tags.TryAddTag(ErrorType, exception.GetType().FullName); } // Add custom tags for duration. @@ -138,6 +157,8 @@ private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exce public void ConnectionRejected(in ConnectionMetricsContext metricsContext) { + AddConnectionEndReason(metricsContext, ConnectionEndReason.MaxConcurrentConnectionsExceeded); + // Check live rather than cached state because this is just a counter, it's not a start/stop event like the other metrics. if (_rejectedConnectionsCounter.Enabled) { @@ -301,7 +322,7 @@ private void TlsHandshakeStopCore(in ConnectionMetricsContext metricsContext, lo } if (exception != null) { - tags.Add("error.type", exception.GetType().FullName); + tags.TryAddTag("error.type", exception.GetType().FullName); } var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); @@ -352,9 +373,16 @@ private static void InitializeConnectionTags(ref TagList tags, in ConnectionMetr public ConnectionMetricsContext CreateContext(BaseConnectionContext connection) { // Cache the state at the start of the connection so we produce consistent start/stop events. - return new ConnectionMetricsContext(connection, - _activeConnectionsCounter.Enabled, _connectionDuration.Enabled, _queuedConnectionsCounter.Enabled, - _queuedRequestsCounter.Enabled, _currentUpgradedRequestsCounter.Enabled, _activeTlsHandshakesCounter.Enabled); + return new ConnectionMetricsContext + { + ConnectionContext = connection, + CurrentConnectionsCounterEnabled = _activeConnectionsCounter.Enabled, + ConnectionDurationEnabled = _connectionDuration.Enabled, + QueuedConnectionsCounterEnabled = _queuedConnectionsCounter.Enabled, + QueuedRequestsCounterEnabled = _queuedRequestsCounter.Enabled, + CurrentUpgradedRequestsCounterEnabled = _currentUpgradedRequestsCounter.Enabled, + CurrentTlsHandshakesCounterEnabled = _activeTlsHandshakesCounter.Enabled + }; } public static bool TryGetHandshakeProtocol(SslProtocols protocols, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out string? version) @@ -398,4 +426,95 @@ public static bool TryGetHandshakeProtocol(SslProtocols protocols, [NotNullWhen( version = null; return false; } + + public static void AddConnectionEndReason(IConnectionMetricsTagsFeature? feature, ConnectionEndReason reason) + { + Debug.Assert(reason != ConnectionEndReason.Unset); + + if (feature != null) + { + if (TryGetErrorType(reason, out var errorTypeValue)) + { + feature.TryAddTag(ErrorType, errorTypeValue); + } + } + } + + public static void AddConnectionEndReason(ConnectionMetricsContext? context, ConnectionEndReason reason, bool overwrite = false) + { + Debug.Assert(reason != ConnectionEndReason.Unset); + + if (context != null) + { + if (TryGetErrorType(reason, out _)) + { + if (context.ConnectionEndReason == null || overwrite) + { + context.ConnectionEndReason = reason; + } + } + } + } + + internal static string? GetErrorType(ConnectionEndReason reason) + { + TryGetErrorType(reason, out var errorTypeValue); + return errorTypeValue; + } + + internal static bool TryGetErrorType(ConnectionEndReason reason, [NotNullWhen(true)]out string? errorTypeValue) + { + errorTypeValue = reason switch + { + ConnectionEndReason.Unset => null, // Not an error + ConnectionEndReason.ClientGoAway => null, // Not an error + ConnectionEndReason.TransportCompleted => null, // Not an error + ConnectionEndReason.GracefulAppShutdown => null, // Not an error + ConnectionEndReason.ConnectionReset => "connection_reset", + ConnectionEndReason.FlowControlWindowExceeded => "flow_control_window_exceeded", + ConnectionEndReason.KeepAliveTimeout => "keep_alive_timeout", + ConnectionEndReason.InsufficientTlsVersion => "insufficient_tls_version", + ConnectionEndReason.InvalidHandshake => "invalid_handshake", + ConnectionEndReason.InvalidStreamId => "invalid_stream_id", + ConnectionEndReason.FrameAfterStreamClose => "frame_after_stream_close", + ConnectionEndReason.UnknownStream => "unknown_stream", + ConnectionEndReason.UnsupportedFrame => "unsupported_frame", + ConnectionEndReason.UnexpectedFrame => "unexpected_frame", + ConnectionEndReason.InvalidFrameLength => "invalid_frame_length", + ConnectionEndReason.InvalidDataPadding => "invalid_data_padding", + ConnectionEndReason.InvalidRequestHeaders => "invalid_request_headers", + ConnectionEndReason.StreamResetLimitExceeded => "stream_reset_limit_exceeded", + ConnectionEndReason.WindowUpdateSizeInvalid => "window_update_size_invalid", + ConnectionEndReason.StreamSelfDependency => "stream_self_dependency", + ConnectionEndReason.InvalidSettings => "invalid_settings", + ConnectionEndReason.MissingStreamEnd => "missing_stream_end", + ConnectionEndReason.MaxFrameLengthExceeded => "max_frame_length_exceeded", + ConnectionEndReason.ErrorReadingHeaders => "error_reading_headers", + ConnectionEndReason.ErrorWritingHeaders => "error_writing_headers", + ConnectionEndReason.OtherError => "other_error", + ConnectionEndReason.InvalidHttpVersion => "invalid_http_version", + ConnectionEndReason.RequestHeadersTimeout => "request_headers_timeout", + ConnectionEndReason.MinRequestBodyDataRate => "min_request_body_data_rate", + ConnectionEndReason.MinResponseDataRate => "min_response_data_rate", + ConnectionEndReason.FlowControlQueueSizeExceeded => "flow_control_queue_size_exceeded", + ConnectionEndReason.OutputQueueSizeExceeded => "output_queue_size_exceeded", + ConnectionEndReason.ClosedCriticalStream => "closed_critical_stream", + ConnectionEndReason.AbortedByApp => "aborted_by_app", + ConnectionEndReason.WriteCanceled => "write_canceled", + ConnectionEndReason.BodyReaderInvalidState => "body_reader_invalid_state", + ConnectionEndReason.ServerTimeout => "server_timeout", + ConnectionEndReason.StreamCreationError => "stream_creation_error", + ConnectionEndReason.IOError => "io_error", + ConnectionEndReason.AppShutdown => "app_shutdown", + ConnectionEndReason.TlsHandshakeFailed => "tls_handshake_failed", + ConnectionEndReason.InvalidRequestLine => "invalid_request_line", + ConnectionEndReason.TlsOverHttp => "tls_over_http", + ConnectionEndReason.MaxRequestBodySizeExceeded => "max_request_body_size_exceeded", + ConnectionEndReason.UnexpectedEndOfRequestContent => "unexpected_end_of_request_content", + ConnectionEndReason.MaxConcurrentConnectionsExceeded => "max_concurrent_connections_exceeded", + _ => throw new InvalidOperationException($"Unable to calculate whether {reason} resolves to error.type value.") + }; + + return errorTypeValue != null; + } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs index e37583755531..25428d10bccd 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/KestrelTrace.General.cs @@ -69,6 +69,11 @@ public void RequestAborted(string connectionId, string traceIdentifier) GeneralLog.RequestAbortedException(_generalLogger, connectionId, traceIdentifier); } + public void RequestBodyDrainBodyReaderInvalidState(string connectionId, string traceIdentifier, Exception ex) + { + GeneralLog.RequestBodyDrainBodyReaderInvalidState(_generalLogger, connectionId, traceIdentifier, ex); + } + private static partial class GeneralLog { [LoggerMessage(13, LogLevel.Error, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": An unhandled exception was thrown by the application.", EventName = "ApplicationError")] @@ -107,6 +112,9 @@ private static partial class GeneralLog [LoggerMessage(66, LogLevel.Debug, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": The request was aborted by the client.", EventName = "RequestAborted")] public static partial void RequestAbortedException(ILogger logger, string connectionId, string traceIdentifier); + [LoggerMessage(67, LogLevel.Information, @"Connection id ""{ConnectionId}"", Request id ""{TraceIdentifier}"": automatic draining of the request body failed because the body reader is in an invalid state.", EventName = "RequestBodyDrainBodyReaderInvalidState")] + public static partial void RequestBodyDrainBodyReaderInvalidState(ILogger logger, string connectionId, string traceIdentifier, Exception ex); + // IDs prior to 64 are reserved for back compat (the various KestrelTrace loggers used to share a single sequence) } } 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 2a9b8863969c..9a4d4eaa4930 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/TimingPipeFlusher.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/TimingPipeFlusher.cs @@ -92,7 +92,7 @@ private async ValueTask TimeFlushAsyncAwaited(ValueTask AbortAllConnectionsAsync() { if (kvp.Value.TryGetConnection(out var connection)) { + // Connection didn't shutdown in allowed time. Force close the connection and set the end reason. + KestrelMetrics.AddConnectionEndReason( + connection.TransportConnection.Features.Get()?.MetricsContext, + ConnectionEndReason.AppShutdown, overwrite: true); + connection.TransportConnection.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedDuringServerShutdown)); abortTasks.Add(connection.ExecutionTask); } diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index 1576c0f5a653..6c0bf1237af4 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs index 28649eeb95af..e365c99a9398 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpMultiplexedConnectionMiddleware.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal; @@ -29,6 +30,7 @@ public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext) var memoryPoolFeature = connectionContext.Features.Get(); var localEndPoint = connectionContext.LocalEndPoint as IPEndPoint; var altSvcHeader = _addAltSvcHeader && localEndPoint != null ? HttpUtilities.GetEndpointAltSvc(localEndPoint, _protocols) : null; + var metricContext = connectionContext.Features.GetRequiredFeature().MetricsContext; var httpConnectionContext = new HttpMultiplexedConnectionContext( connectionContext.ConnectionId, @@ -39,7 +41,8 @@ public Task OnConnectionAsync(MultiplexedConnectionContext connectionContext) connectionContext.Features, memoryPoolFeature?.MemoryPool ?? System.Buffers.MemoryPool.Shared, localEndPoint, - connectionContext.RemoteEndPoint as IPEndPoint); + connectionContext.RemoteEndPoint as IPEndPoint, + metricContext); if (connectionContext.Features.Get() is { } metricsTags) { diff --git a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs index f986ee41c383..4508204d7fe7 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs @@ -154,6 +154,7 @@ public async Task OnConnectionAsync(ConnectionContext context) context.Features.Set(feature); context.Features.Set(sslStream); // Anti-pattern, but retain for back compat + var metricsTagsFeature = context.Features.Get(); var metricsContext = context.Features.GetRequiredFeature().MetricsContext; var startTimestamp = Stopwatch.GetTimestamp(); try @@ -173,7 +174,7 @@ public async Task OnConnectionAsync(ConnectionContext context) } catch (OperationCanceledException ex) { - RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, ex); + RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, metricsTagsFeature, ex); _logger.AuthenticationTimedOut(); await sslStream.DisposeAsync(); @@ -181,7 +182,7 @@ public async Task OnConnectionAsync(ConnectionContext context) } catch (IOException ex) { - RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, ex); + RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, metricsTagsFeature, ex); _logger.AuthenticationFailed(ex); await sslStream.DisposeAsync(); @@ -189,10 +190,9 @@ public async Task OnConnectionAsync(ConnectionContext context) } catch (AuthenticationException ex) { - RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, ex); + RecordHandshakeFailed(_metrics, startTimestamp, Stopwatch.GetTimestamp(), metricsContext, metricsTagsFeature, ex); _logger.AuthenticationFailed(ex); - await sslStream.DisposeAsync(); return; } @@ -202,15 +202,16 @@ public async Task OnConnectionAsync(ConnectionContext context) _logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol); - if (context.Features.Get() is { } metricsTags) + if (metricsTagsFeature != null) { if (KestrelMetrics.TryGetHandshakeProtocol(sslStream.SslProtocol, out var protocolName, out var protocolVersion)) { + // "tls" is considered the default protocol name and isn't explicitly recorded. if (protocolName != "tls") { - metricsTags.Tags.Add(new KeyValuePair("tls.protocol.name", protocolName)); + metricsTagsFeature.Tags.Add(new KeyValuePair("tls.protocol.name", protocolName)); } - metricsTags.Tags.Add(new KeyValuePair("tls.protocol.version", protocolVersion)); + metricsTagsFeature.Tags.Add(new KeyValuePair("tls.protocol.version", protocolVersion)); } } @@ -235,10 +236,12 @@ await using (sslDuplexPipe) context.Transport = originalTransport; } - static void RecordHandshakeFailed(KestrelMetrics metrics, long startTimestamp, long currentTimestamp, ConnectionMetricsContext metricsContext, Exception ex) + static void RecordHandshakeFailed(KestrelMetrics metrics, long startTimestamp, long currentTimestamp, ConnectionMetricsContext metricsContext, IConnectionMetricsTagsFeature? metricsTagsFeature, Exception ex) { KestrelEventSource.Log.TlsHandshakeFailed(metricsContext.ConnectionContext.ConnectionId); KestrelEventSource.Log.TlsHandshakeStop(metricsContext.ConnectionContext, null); + + KestrelMetrics.AddConnectionEndReason(metricsTagsFeature, ConnectionEndReason.TlsHandshakeFailed); metrics.TlsHandshakeStop(metricsContext, startTimestamp, currentTimestamp, exception: ex); } } diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs index bf905511dd87..f6dc45897529 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTests.cs @@ -246,7 +246,7 @@ static bool TryReadResponse(ReadResult read, out SequencePosition consumed, out } Assert.Equal($"{connectionId}:{count:X8}", feature.TraceIdentifier); - _http1Connection.StopProcessingNextRequest(); + _http1Connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdown); await requestProcessingTask.DefaultTimeout(); } @@ -572,7 +572,7 @@ public async Task ProcessRequestsAsyncEnablesKeepAliveTimeout() var expectedKeepAliveTimeout = _serviceContext.ServerOptions.Limits.KeepAliveTimeout; _timeoutControl.Verify(cc => cc.SetTimeout(expectedKeepAliveTimeout, TimeoutReason.KeepAlive)); - _http1Connection.StopProcessingNextRequest(); + _http1Connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdown); _application.Output.Complete(); await requestProcessingTask.DefaultTimeout(); @@ -657,7 +657,7 @@ public async Task RequestProcessingTaskIsUnwrapped() var data = Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n"); await _application.Output.WriteAsync(data); - _http1Connection.StopProcessingNextRequest(); + _http1Connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdown); Assert.IsNotType>(requestProcessingTask); await requestProcessingTask.DefaultTimeout(); @@ -680,7 +680,7 @@ public async Task RequestAbortedTokenIsResetBeforeLastWriteWithContentLength() await _http1Connection.WriteAsync(new ArraySegment(new[] { (byte)'d' })); Assert.NotEqual(original, _http1Connection.RequestAborted); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); Assert.False(original.IsCancellationRequested); Assert.False(_http1Connection.RequestAborted.IsCancellationRequested); @@ -702,7 +702,7 @@ public async Task RequestAbortedTokenIsResetBeforeLastWriteAsyncWithContentLengt await _http1Connection.WriteAsync(new ArraySegment(new[] { (byte)'d' }), default(CancellationToken)); Assert.NotEqual(original, _http1Connection.RequestAborted); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); Assert.False(original.IsCancellationRequested); Assert.False(_http1Connection.RequestAborted.IsCancellationRequested); @@ -717,7 +717,7 @@ public async void BodyWriter_OnAbortedConnection_ReturnsFlushResultWithIsComplet var successResult = await writer.WriteAsync(payload); Assert.False(successResult.IsCompleted); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); var failResult = await _http1Connection.FlushPipeAsync(new CancellationToken()); Assert.True(failResult.IsCompleted); } @@ -762,7 +762,7 @@ public async Task RequestAbortedTokenIsResetBeforeLastWriteAsyncAwaitedWithConte await _http1Connection.WriteAsync(new ArraySegment(new[] { (byte)'d' }), default(CancellationToken)); Assert.NotEqual(original, _http1Connection.RequestAborted); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); Assert.False(original.IsCancellationRequested); Assert.False(_http1Connection.RequestAborted.IsCancellationRequested); @@ -780,7 +780,7 @@ public async Task RequestAbortedTokenIsResetBeforeLastWriteWithChunkedEncoding() await _http1Connection.ProduceEndAsync(); Assert.NotEqual(original, _http1Connection.RequestAborted); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); Assert.False(original.IsCancellationRequested); Assert.False(_http1Connection.RequestAborted.IsCancellationRequested); @@ -792,7 +792,7 @@ public void RequestAbortedTokenIsFullyUsableAfterCancellation() var originalToken = _http1Connection.RequestAborted; var originalRegistration = originalToken.Register(() => { }); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); Assert.True(originalToken.WaitHandle.WaitOne(TestConstants.DefaultTimeout)); Assert.True(_http1Connection.RequestAborted.WaitHandle.WaitOne(TestConstants.DefaultTimeout)); @@ -806,7 +806,7 @@ public void RequestAbortedTokenIsUsableAfterCancellation() var originalToken = _http1Connection.RequestAborted; var originalRegistration = originalToken.Register(() => { }); - _http1Connection.Abort(new ConnectionAbortedException()); + _http1Connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); // The following line will throw an ODE because the original CTS backing the token has been diposed. // See https://github.com/dotnet/aspnetcore/pull/4447 for the history behind this test. diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs index ba86cff87fb6..5134fd9d0ddd 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs @@ -38,8 +38,12 @@ public override void Initialize(TestContext context, MethodInfo methodInfo, obje _transport = pair.Transport; _application = pair.Application; + var connectionContext = Mock.Of(); + var metricsContext = TestContextFactory.CreateMetricsContext(connectionContext); + var connectionFeatures = new FeatureCollection(); connectionFeatures.Set(Mock.Of()); + connectionFeatures.Set(new TestConnectionMetricsContextFeature { MetricsContext = metricsContext }); _serviceContext = new TestServiceContext(LoggerFactory) { @@ -53,7 +57,8 @@ public override void Initialize(TestContext context, MethodInfo methodInfo, obje transport: pair.Transport, timeoutControl: _timeoutControl.Object, memoryPool: _pipelineFactory, - connectionFeatures: connectionFeatures); + connectionFeatures: connectionFeatures, + metricsContext: metricsContext); _http1Connection = new TestHttp1Connection(_http1ConnectionContext); } diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1HttpProtocolFeatureCollectionTests.cs index 33200c697196..af82ee077f53 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1HttpProtocolFeatureCollectionTests.cs @@ -26,12 +26,19 @@ public class Http1HttpProtocolFeatureCollectionTests public Http1HttpProtocolFeatureCollectionTests() { + var connectionContext = Mock.Of(); + var metricsContext = TestContextFactory.CreateMetricsContext(connectionContext); + + var connectionFeatures = new FeatureCollection(); + connectionFeatures.Set(new TestConnectionMetricsContextFeature { MetricsContext = metricsContext }); + var context = TestContextFactory.CreateHttpConnectionContext( - connectionContext: Mock.Of(), + connectionContext: connectionContext, serviceContext: new TestServiceContext(), transport: Mock.Of(), - connectionFeatures: new FeatureCollection(), - timeoutControl: Mock.Of()); + connectionFeatures: connectionFeatures, + timeoutControl: Mock.Of(), + metricsContext: metricsContext); _httpConnectionContext = context; _http1Connection = new TestHttp1Connection(context); diff --git a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs index 8b38ec829ef1..b927baefa60f 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1OutputProducerTests.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.InternalTesting; using Moq; using Xunit; +using Microsoft.AspNetCore.Connections.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -117,20 +118,23 @@ public async Task FlushAsync_OnSocketWithCanceledPendingFlush_ReturnsResultWithI public void AbortsTransportEvenAfterDispose() { var mockConnectionContext = new Mock(); + var metricsContext = new ConnectionMetricsContext { ConnectionContext = mockConnectionContext.Object }; - var outputProducer = CreateOutputProducer(connectionContext: mockConnectionContext.Object); + var outputProducer = CreateOutputProducer(connectionContext: mockConnectionContext.Object, metricsContext: metricsContext); outputProducer.Dispose(); mockConnectionContext.Verify(f => f.Abort(It.IsAny()), Times.Never()); - outputProducer.Abort(null); + outputProducer.Abort(null, ConnectionEndReason.AbortedByApp); mockConnectionContext.Verify(f => f.Abort(null), Times.Once()); - outputProducer.Abort(null); + outputProducer.Abort(null, ConnectionEndReason.AbortedByApp); mockConnectionContext.Verify(f => f.Abort(null), Times.Once()); + + Assert.Equal(ConnectionEndReason.AbortedByApp, metricsContext.ConnectionEndReason); } [Fact] @@ -218,7 +222,8 @@ public void ReusesFakeMemory() private TestHttpOutputProducer CreateOutputProducer( PipeOptions pipeOptions = null, - ConnectionContext connectionContext = null) + ConnectionContext connectionContext = null, + ConnectionMetricsContext metricsContext = null) { pipeOptions = pipeOptions ?? new PipeOptions(); connectionContext = connectionContext ?? Mock.Of(); @@ -233,15 +238,21 @@ public void ReusesFakeMemory() serviceContext.Log, Mock.Of(), Mock.Of(), + metricsContext ?? new ConnectionMetricsContext { ConnectionContext = connectionContext }, Mock.Of()); return socketOutput; } + private sealed class TestConnectionMetricsContextFeature : IConnectionMetricsContextFeature + { + public ConnectionMetricsContext MetricsContext { get; } + } + private class TestHttpOutputProducer : Http1OutputProducer { - public TestHttpOutputProducer(Pipe pipe, string connectionId, ConnectionContext connectionContext, MemoryPool memoryPool, KestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature, IHttpOutputAborter outputAborter) - : base(pipe.Writer, connectionId, connectionContext, memoryPool, log, timeoutControl, minResponseDataRateFeature, outputAborter) + public TestHttpOutputProducer(Pipe pipe, string connectionId, ConnectionContext connectionContext, MemoryPool memoryPool, KestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature, ConnectionMetricsContext metricsContext, IHttpOutputAborter outputAborter) + : base(pipe.Writer, connectionId, connectionContext, memoryPool, log, timeoutControl, minResponseDataRateFeature, metricsContext, outputAborter) { Pipe = pipe; } diff --git a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs index 5a85823a88cf..4862831210c7 100644 --- a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Primitives; using Moq; using Xunit; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -28,12 +29,20 @@ public void InitialDictionaryIsEmpty() { var options = new PipeOptions(memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(options, options); + + var connectionContext = Mock.Of(); + var metricsContext = TestContextFactory.CreateMetricsContext(connectionContext); + + var connectionFeatures = new FeatureCollection(); + connectionFeatures.Set(new TestConnectionMetricsContextFeature { MetricsContext = metricsContext }); + var http1ConnectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: new TestServiceContext(), - connectionContext: Mock.Of(), + connectionContext: connectionContext, transport: pair.Transport, memoryPool: memoryPool, - connectionFeatures: new FeatureCollection()); + connectionFeatures: connectionFeatures, + metricsContext: metricsContext); var http1Connection = new Http1Connection(http1ConnectionContext); diff --git a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs index 1bb8795d1395..914fa9bb5f58 100644 --- a/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs +++ b/src/Servers/Kestrel/Core/test/TestHelpers/TestInput.cs @@ -30,19 +30,24 @@ public TestInput(KestrelTrace log = null, ITimeoutControl timeoutControl = null) Transport = pair.Transport; Application = pair.Application; + var connectionContext = Mock.Of(); + var metricsContext = TestContextFactory.CreateMetricsContext(connectionContext); + var connectionFeatures = new FeatureCollection(); connectionFeatures.Set(Mock.Of()); + connectionFeatures.Set(new TestConnectionMetricsContextFeature { MetricsContext = metricsContext }); Http1ConnectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: new TestServiceContext { Log = log ?? new KestrelTrace(NullLoggerFactory.Instance) }, - connectionContext: Mock.Of(), + connectionContext: connectionContext, transport: Transport, timeoutControl: timeoutControl ?? Mock.Of(), memoryPool: _memoryPool, - connectionFeatures: connectionFeatures); + connectionFeatures: connectionFeatures, + metricsContext: metricsContext); Http1Connection = new Http1Connection(Http1ConnectionContext); Http1Connection.HttpResponseControl = Mock.Of(); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs index 204019ca9014..8c4f22ddfc42 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/Http2/Http2ConnectionBenchmarkBase.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Http2HeadersEnumerator = Microsoft.AspNetCore.Server.Kestrel.Core.Tests.Http2HeadersEnumerator; +using Microsoft.AspNetCore.Connections.Features; namespace Microsoft.AspNetCore.Server.Kestrel.Microbenchmarks; @@ -75,6 +76,8 @@ public virtual void GlobalSetup() var featureCollection = new FeatureCollection(); featureCollection.Set(new TestConnectionMetricsContextFeature()); + featureCollection.Set(new TestConnectionMetricsTagsFeature()); + featureCollection.Set(new TestProtocolErrorCodeFeature()); var connectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: serviceContext, connectionContext: null, @@ -191,4 +194,14 @@ private sealed class TestConnectionMetricsContextFeature : IConnectionMetricsCon { public ConnectionMetricsContext MetricsContext { get; } } + + private sealed class TestConnectionMetricsTagsFeature : IConnectionMetricsTagsFeature + { + public ICollection> Tags { get; } + } + + private sealed class TestProtocolErrorCodeFeature : IProtocolErrorCodeFeature + { + public long Error { get; set; } = -1; + } } diff --git a/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs b/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs index beded5607dee..8830c9331005 100644 --- a/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs +++ b/src/Servers/Kestrel/samples/Http2SampleApp/Program.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; using System.IO; using System.Security.Authentication; using Microsoft.AspNetCore.Connections; @@ -71,6 +72,8 @@ public static void Main(string[] args) factory.AddConsole(); }); + Console.WriteLine($"Process ID: {Environment.ProcessId}"); + hostBuilder.Build().Run(); } } diff --git a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs index 3bc7d793be57..e387534249d1 100644 --- a/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs +++ b/src/Servers/Kestrel/shared/test/Http3/Http3InMemory.cs @@ -93,6 +93,8 @@ public void OnTimeout(TimeoutReason reason) internal TestMultiplexedConnectionContext MultiplexedConnectionContext { get; set; } + internal IDictionary ConnectionTags => MultiplexedConnectionContext.Tags.ToDictionary(t => t.Key, t => t.Value); + internal long GetStreamId(long mask) { var id = (_currentStreamId << 2) | mask; @@ -228,6 +230,8 @@ public async Task InitializeConnectionAsync(RequestDelegate application) ConnectionId = "TEST" }; + var metricsContext = MultiplexedConnectionContext.Features.GetRequiredFeature().MetricsContext; + var httpConnectionContext = new HttpMultiplexedConnectionContext( connectionId: MultiplexedConnectionContext.ConnectionId, HttpProtocols.Http3, @@ -237,7 +241,8 @@ public async Task InitializeConnectionAsync(RequestDelegate application) serviceContext: _serviceContext, memoryPool: _memoryPool, localEndPoint: null, - remoteEndPoint: null); + remoteEndPoint: null, + metricsContext: metricsContext); httpConnectionContext.TimeoutControl = _timeoutControl; _httpConnection = new HttpConnection(httpConnectionContext); @@ -981,7 +986,7 @@ internal void VerifyGoAway(Http3FrameWithPayload frame, long expectedLastStreamI } } -internal class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, IProtocolErrorCodeFeature, IConnectionMetricsContextFeature +internal class TestMultiplexedConnectionContext : MultiplexedConnectionContext, IConnectionLifetimeNotificationFeature, IConnectionLifetimeFeature, IConnectionHeartbeatFeature, IProtocolErrorCodeFeature, IConnectionMetricsContextFeature, IConnectionMetricsTagsFeature { public readonly Channel ToServerAcceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -1006,7 +1011,10 @@ public TestMultiplexedConnectionContext(Http3InMemory testBase) Features.Set(this); Features.Set(this); Features.Set(this); + Features.Set(this); ConnectionClosedRequested = ConnectionClosingCts.Token; + + MetricsContext = TestContextFactory.CreateMetricsContext(this); } public override string ConnectionId { get; set; } @@ -1024,8 +1032,11 @@ public long Error get => _error ?? -1; set => _error = value; } + public ConnectionMetricsContext MetricsContext { get; } + public ICollection> Tags { get; } = new List>(); + public override void Abort() { Abort(new ConnectionAbortedException()); diff --git a/src/Servers/Kestrel/shared/test/TestConnectionMetricsContextFeature.cs b/src/Servers/Kestrel/shared/test/TestConnectionMetricsContextFeature.cs new file mode 100644 index 000000000000..1a328bd2d85c --- /dev/null +++ b/src/Servers/Kestrel/shared/test/TestConnectionMetricsContextFeature.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +namespace Microsoft.AspNetCore.InternalTesting; + +internal sealed class TestConnectionMetricsContextFeature : IConnectionMetricsContextFeature +{ + public ConnectionMetricsContext MetricsContext { get; init; } +} diff --git a/src/Servers/Kestrel/shared/test/TestContextFactory.cs b/src/Servers/Kestrel/shared/test/TestContextFactory.cs index 1bb026b776af..3baf69b6348a 100644 --- a/src/Servers/Kestrel/shared/test/TestContextFactory.cs +++ b/src/Servers/Kestrel/shared/test/TestContextFactory.cs @@ -53,7 +53,8 @@ internal static class TestContextFactory MemoryPool memoryPool = null, IPEndPoint localEndPoint = null, IPEndPoint remoteEndPoint = null, - ITimeoutControl timeoutControl = null) + ITimeoutControl timeoutControl = null, + ConnectionMetricsContext metricsContext = null) { var context = new HttpConnectionContext( "TestConnectionId", @@ -65,7 +66,7 @@ internal static class TestContextFactory memoryPool ?? MemoryPool.Shared, localEndPoint, remoteEndPoint, - CreateMetricsContext(connectionContext)); + metricsContext ?? CreateMetricsContext(connectionContext)); context.TimeoutControl = timeoutControl; context.Transport = transport; @@ -79,18 +80,23 @@ internal static class TestContextFactory MemoryPool memoryPool = null, IPEndPoint localEndPoint = null, IPEndPoint remoteEndPoint = null, - ITimeoutControl timeoutControl = null) + ITimeoutControl timeoutControl = null, + ConnectionMetricsContext metricsContext = null) { + connectionContext ??= new TestMultiplexedConnectionContext { ConnectionId = "TEST" }; + metricsContext ??= new ConnectionMetricsContext { ConnectionContext = connectionContext }; + var http3ConnectionContext = new HttpMultiplexedConnectionContext( "TEST", HttpProtocols.Http3, altSvcHeader: null, - connectionContext ?? new TestMultiplexedConnectionContext { ConnectionId = "TEST" }, + connectionContext, serviceContext ?? CreateServiceContext(new KestrelServerOptions()), connectionFeatures ?? new FeatureCollection(), memoryPool ?? PinnedBlockMemoryPoolFactory.Create(), localEndPoint, - remoteEndPoint) + remoteEndPoint, + metricsContext) { TimeoutControl = timeoutControl }; @@ -214,11 +220,9 @@ internal static class TestContextFactory }; } - public static ConnectionMetricsContext CreateMetricsContext(ConnectionContext connectionContext) + public static ConnectionMetricsContext CreateMetricsContext(BaseConnectionContext connectionContext) { - return new ConnectionMetricsContext(connectionContext, - currentConnectionsCounterEnabled: false, connectionDurationEnabled: false, queuedConnectionsCounterEnabled: false, - queuedRequestsCounterEnabled: false, currentUpgradedRequestsCounterEnabled: false, currentTlsHandshakesCounterEnabled: false); + return new ConnectionMetricsContext { ConnectionContext = connectionContext }; } private class TestHttp2StreamLifetimeHandler : IHttp2StreamLifetimeHandler diff --git a/src/Servers/Kestrel/shared/test/TestServiceContext.cs b/src/Servers/Kestrel/shared/test/TestServiceContext.cs index b8681b784a52..85692f1fe341 100644 --- a/src/Servers/Kestrel/shared/test/TestServiceContext.cs +++ b/src/Servers/Kestrel/shared/test/TestServiceContext.cs @@ -64,8 +64,11 @@ private void Initialize(ILoggerFactory loggerFactory, KestrelTrace kestrelTrace, DateHeaderValueManager.OnHeartbeat(); Metrics = metrics; + ShutdownTimeout = TestConstants.DefaultTimeout; } + public TimeSpan ShutdownTimeout { get; set; } + public ILoggerFactory LoggerFactory { get; set; } public FakeTimeProvider FakeTimeProvider { get; set; } diff --git a/src/Servers/Kestrel/test/FunctionalTests/ConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ConnectionMiddlewareTests.cs index f4b0af365b16..ccc023b7be00 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ConnectionMiddlewareTests.cs @@ -1,17 +1,11 @@ // 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.Collections.Concurrent; -using System.IO; using System.IO.Pipelines; using System.Net; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; -using Microsoft.AspNetCore.InternalTesting; -using Xunit; #if SOCKETS namespace Microsoft.AspNetCore.Server.Kestrel.Sockets.FunctionalTests; diff --git a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs index b4df79959dd0..84e253b66400 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs @@ -30,6 +30,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Xunit; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using System.Diagnostics.Metrics; #if SOCKETS namespace Microsoft.AspNetCore.Server.Kestrel.Sockets.FunctionalTests; @@ -583,6 +585,9 @@ public async Task ThrowsOnReadAfterConnectionError() [Fact] public async Task RequestAbortedTokenFiredOnClientFIN() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var appStarted = new SemaphoreSlim(0); var requestAborted = new SemaphoreSlim(0); var builder = TransportSelector.GetHostBuilder() @@ -600,7 +605,8 @@ public async Task RequestAbortedTokenFiredOnClientFIN() await requestAborted.WaitAsync().DefaultTimeout(); })); }) - .ConfigureServices(AddTestLogging); + .ConfigureServices(AddTestLogging) + .ConfigureServices(s => s.AddSingleton(testMeterFactory)); using (var host = builder.Build()) { @@ -617,6 +623,11 @@ public async Task RequestAbortedTokenFiredOnClientFIN() await host.StopAsync(); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } [Fact] @@ -669,6 +680,9 @@ public async Task RequestAbortedTokenUnchangedOnAbort() [InlineData(false)] public async Task AbortingTheConnection(bool fin) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var builder = TransportSelector.GetHostBuilder() .ConfigureWebHost(webHostBuilder => { @@ -688,7 +702,8 @@ public async Task AbortingTheConnection(bool fin) return Task.CompletedTask; })); }) - .ConfigureServices(AddTestLogging); + .ConfigureServices(AddTestLogging) + .ConfigureServices(s => s.AddSingleton(testMeterFactory)); using (var host = builder.Build()) { @@ -711,6 +726,11 @@ public async Task AbortingTheConnection(bool fin) await host.StopAsync(); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.AbortedByApp), (string)m.Tags[KestrelMetrics.ErrorType]); + }); } [Theory] diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index eb01e1eebe09..7a329d72a7f4 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs @@ -1,36 +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; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.Server.Kestrel.Https; -using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; -using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; -using Moq; -using Xunit; #if SOCKETS namespace Microsoft.AspNetCore.Server.Kestrel.Sockets.FunctionalTests; @@ -207,6 +200,11 @@ public async Task ThrowsOnWriteWithRequestAbortedTokenAfterRequestIsAborted(List var requestAbortedWh = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestStartWh = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testServiceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); + await using (var server = new TestServer(async httpContext => { requestStartWh.SetResult(); @@ -233,7 +231,7 @@ public async Task ThrowsOnWriteWithRequestAbortedTokenAfterRequestIsAborted(List } writeTcs.SetException(new Exception("This shouldn't be reached.")); - }, new TestServiceContext(LoggerFactory), listenOptions)) + }, testServiceContext, listenOptions)) { using (var connection = server.CreateConnection()) { @@ -254,6 +252,11 @@ public async Task ThrowsOnWriteWithRequestAbortedTokenAfterRequestIsAborted(List // RequestAborted tripped await requestAbortedWh.Task.DefaultTimeout(); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } [Theory] @@ -427,7 +430,10 @@ public async Task ClientAbortingConnectionImmediatelyIsNotLoggedHigherThanDebug( // There's no guarantee that the app even gets invoked in this test. The connection reset can be observed // as early as accept. - var testServiceContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testServiceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = new TestServer(context => Task.CompletedTask, testServiceContext, listenOptions)) { for (var i = 0; i < numConnections; i++) @@ -453,6 +459,11 @@ await using (var server = new TestServer(context => Task.CompletedTask, testServ Assert.Empty(transportLogs.Where(w => w.LogLevel > LogLevel.Debug)); Assert.Empty(coreLogs.Where(w => w.LogLevel > LogLevel.Information)); + + await connectionDuration.WaitForMeasurementsAsync(minCount: 1).DefaultTimeout(); + + var measurement = connectionDuration.GetMeasurementSnapshot().First(); + Assert.DoesNotContain(KestrelMetrics.ErrorType, measurement.Tags.Keys); } [Theory] @@ -492,7 +503,10 @@ public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool } }; - var testContext = new TestServiceContext(LoggerFactory) + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ServerOptions = { @@ -584,6 +598,11 @@ async Task App(HttpContext context) logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinResponseDataRate), (string)m.Tags[KestrelMetrics.ErrorType]); + }); } [Theory] @@ -742,7 +761,10 @@ public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressu } }; - var testContext = new TestServiceContext(LoggerFactory) + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ServerOptions = { @@ -827,6 +849,11 @@ async Task App(HttpContext context) await AssertStreamAborted(connection.Stream, responseSize); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinResponseDataRate), (string)m.Tags[KestrelMetrics.ErrorType]); + }); } [ConditionalFact] @@ -1000,7 +1027,10 @@ public async Task ClientCanReceiveFullConnectionCloseResponseWithoutErrorAtALowD var requestAborted = false; var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var testContext = new TestServiceContext(LoggerFactory) + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ServerOptions = { @@ -1063,6 +1093,11 @@ await using (var server = new TestServer(App, testContext, listenOptions)) Assert.Equal(0, TestSink.Writes.Count(w => w.EventId.Name == "ResponseMinimumDataRateNotSatisfied")); Assert.Equal(1, TestSink.Writes.Count(w => w.EventId.Name == "ConnectionStop")); Assert.False(requestAborted); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } private async Task AssertStreamAborted(Stream stream, int totalBytes) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs index a6f7de345e6f..77ef4088f6fa 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/BadHttpRequestTests.cs @@ -15,6 +15,7 @@ using Moq; using Xunit; using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -226,7 +227,10 @@ await using (var server = new TestServer(context => Task.CompletedTask, new Test [Fact] public async Task BadRequestForHttp2() { - await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)))) { using (var client = server.CreateConnection()) { @@ -238,6 +242,11 @@ await using (var server = new TestServer(context => Task.CompletedTask, new Test Assert.Empty(await client.Stream.ReadUntilEndAsync().DefaultTimeout()); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.InvalidHttpVersion), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 1642d31d7a82..576a4058ae41 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -1,20 +1,17 @@ // 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.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; -using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Logging; -using Xunit; using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -852,7 +849,10 @@ public async Task ChunkedNotFinalTransferCodingResultsIn400() [Fact] public async Task ClosingConnectionMidChunkPrefixThrows() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var readStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); #pragma warning disable CS0618 // Type or member is obsolete var exTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -898,6 +898,11 @@ public async Task ClosingConnectionMidChunkPrefixThrows() Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, badReqEx.Reason); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.UnexpectedEndOfRequestContent), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs index 859ecad6373a..296a001132ca 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs @@ -104,6 +104,7 @@ public async Task RejectsConnectionsWhenLimitReached() { var testMeterFactory = new TestMeterFactory(); using var rejectedConnections = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.rejected_connections"); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); const int max = 10; var requestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -149,6 +150,21 @@ public async Task RejectsConnectionsWhenLimitReached() } } + var measurements = connectionDuration.GetMeasurementSnapshot(); + + var connectionErrors = measurements + .GroupBy(m => + { + m.Tags.TryGetValue(KestrelMetrics.ErrorType, out var value); + return value as string; + }) + .ToList(); + + // 10 successful connections. + Assert.Equal(10, connectionErrors.Single(e => e.Key == null).Count()); + // 10 rejected connecitons. + Assert.Equal(10, connectionErrors.Single(e => e.Key == KestrelMetrics.GetErrorType(ConnectionEndReason.MaxConcurrentConnectionsExceeded)).Count()); + static void AssertCounter(CollectedMeasurement measurement) => Assert.Equal(1, measurement.Value); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionMiddlewareTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionMiddlewareTests.cs index 07d6502a7ff4..2f56f8294c64 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionMiddlewareTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionMiddlewareTests.cs @@ -16,6 +16,8 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging.Testing; using Xunit; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -150,10 +152,13 @@ await using (var server = new TestServer(requestDelegate, serviceContext, listen [MemberData(nameof(EchoAppRequestDelegates))] public async Task ImmediateFinAfterThrowingClosesGracefully(RequestDelegate requestDelegate) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); listenOptions.Use(next => context => throw new InvalidOperationException()); - var serviceContext = new TestServiceContext(LoggerFactory); + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = new TestServer(requestDelegate, serviceContext, listenOptions)) { @@ -164,6 +169,11 @@ await using (var server = new TestServer(requestDelegate, serviceContext, listen await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(typeof(InvalidOperationException).FullName, m.Tags[KestrelMetrics.ErrorType]); + }); } [Theory] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index 0a437a4a2c1d..21a1c785aef5 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Moq; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -47,6 +48,7 @@ public async Task MaxConcurrentStreamsLogging_ReachLimit_MessageLogged() Assert.Equal(1, LogMessages.Count(m => m.EventId.Name == "Http2MaxConcurrentStreamsReached")); await StopConnectionAsync(expectedLastStreamId: 5, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -79,6 +81,7 @@ public async Task FlowControl_NoAvailability_ResponseHeadersStillFlushed() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -157,6 +160,7 @@ public async Task FlowControl_OneStream_CorrectlyAwaited() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -206,6 +210,7 @@ public async Task RequestHeaderStringReuse_MultipleStreams_KnownHeaderReused() Assert.Same(path1, path2); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -273,6 +278,7 @@ public async Task RequestHeaderStringReuse_MultipleStreams_KnownHeaderClearedIfN Assert.Equal(StringValues.Empty, contentTypeValue2); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } private class ResponseTrailersWrapper : IHeaderDictionary @@ -390,6 +396,7 @@ public async Task ResponseTrailers_MultipleStreams_Reset() Assert.NotSame(trailersFirst, trailersLast); await StopConnectionAsync(expectedLastStreamId: 5, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -423,6 +430,7 @@ public async Task StreamPool_SingleStream_ReturnedToPool() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -547,6 +555,7 @@ public async Task StreamPool_MultipleStreamsInSequence_PooledStreamReused() withStreamId: 3); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -614,6 +623,7 @@ public async Task StreamPool_EndedStreamErrorsOnStart_NotReturnedToPool() Assert.Equal(0, _connection.StreamPool.Count); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -643,6 +653,7 @@ public async Task StreamPool_UnendedStreamErrorsOnStart_NotReturnedToPool() Assert.Equal(0, _connection.StreamPool.Count); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -684,6 +695,7 @@ public async Task StreamPool_UnusedExpiredStream_RemovedFromPool() Assert.True(((Http2OutputProducer)pooledStream.Output)._disposed); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -701,6 +713,7 @@ public async Task Frame_Received_OverMaxSize_FrameError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorFrameOverLimit(length, Http2PeerSettings.MinAllowedMaxFrameSize)); + AssertConnectionEndReason(ConnectionEndReason.MaxFrameLengthExceeded); } [Fact] @@ -733,6 +746,7 @@ public async Task ServerSettings_ChangesRequestMaxFrameSize() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -757,6 +771,7 @@ public async Task DATA_Received_ReadByStream() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -784,6 +799,7 @@ public async Task DATA_Received_MaxSize_ReadByStream() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -878,6 +894,7 @@ public async Task DATA_Received_GreaterThanInitialWindowSize_ReadByStream() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); foreach (var frame in dataFrames) { @@ -917,6 +934,7 @@ public async Task DATA_Received_RightAtWindowLimit_DoesNotPausePipe() await SendDataAsync(1, new Memory(), endStream: true); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -947,6 +965,7 @@ public async Task DATA_Received_Multiple_ReadByStream() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -1010,6 +1029,7 @@ public async Task DATA_Received_Multiplexed_ReadByStreams() withStreamId: 3); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloBytes.AsSpan().SequenceEqual(stream1DataFrame1.PayloadSequence.ToArray())); Assert.True(_worldBytes.AsSpan().SequenceEqual(stream1DataFrame2.PayloadSequence.ToArray())); @@ -1122,6 +1142,7 @@ public async Task DATA_Received_Multiplexed_GreaterThanInitialWindowSize_ReadByS withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); foreach (var frame in dataFrames) { @@ -1200,6 +1221,7 @@ public async Task DATA_Received_Multiplexed_AppMustNotBlockOtherFrames() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Theory] @@ -1227,6 +1249,7 @@ public async Task DATA_Received_WithPadding_ReadByStream(byte padLength) withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -1296,6 +1319,7 @@ public async Task DATA_Received_WithPadding_CountsTowardsInputFlowControl(byte p withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame3.PayloadSequence.ToArray())); @@ -1336,6 +1360,7 @@ public async Task DATA_Received_ButNotConsumedByApp_CountsTowardsInputFlowContro withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); var updateSize = ((framesConnectionInWindow / 2) + 1) * _maxData.Length; Assert.Equal(updateSize, connectionWindowUpdateFrame.WindowUpdateSizeIncrement); @@ -1427,6 +1452,7 @@ public async Task DATA_BufferRequestBodyLargerThanStreamSizeSmallerThanConnectio withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); foreach (var frame in dataFrames) { @@ -1451,6 +1477,7 @@ public async Task DATA_Received_StreamIdZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.DATA)); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -1465,6 +1492,7 @@ public async Task DATA_Received_StreamIdEven_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.DATA, streamId: 2)); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -1480,6 +1508,7 @@ public async Task DATA_Received_PaddingEqualToFramePayloadLength_ConnectionError expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.DATA)); + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -1495,6 +1524,7 @@ public async Task DATA_Received_PaddingGreaterThanFramePayloadLength_ConnectionE expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.DATA)); + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -1510,6 +1540,7 @@ public async Task DATA_Received_FrameLengthZeroPaddingZero_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.DATA, expectedLength: 1)); + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -1525,6 +1556,7 @@ public async Task DATA_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.DATA, streamId: 1, headersStreamId: 1)); + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -1539,6 +1571,7 @@ public async Task DATA_Received_StreamIdle_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdle(Http2FrameType.DATA, streamId: 1)); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -1556,6 +1589,7 @@ public async Task DATA_Received_StreamHalfClosedRemote_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1)); + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); } [Fact] @@ -1581,6 +1615,7 @@ public async Task DATA_Received_StreamClosed_ConnectionError() CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); } [Fact] @@ -1615,6 +1650,7 @@ public async Task Frame_MultipleStreams_CanBeCreatedIfClientCountIsLessThanActua firstRequestBlock.SetResult(); await StopConnectionAsync(3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -1627,6 +1663,7 @@ public async Task MaxTrackedStreams_SmallMaxConcurrentStreams_LowerLimitOf100Asy Assert.Equal((uint)100, _connection.MaxTrackedStreams); await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -1639,6 +1676,7 @@ public async Task MaxTrackedStreams_DefaultMaxConcurrentStreams_DoubleLimit() Assert.Equal((uint)200, _connection.MaxTrackedStreams); await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -1651,6 +1689,7 @@ public async Task MaxTrackedStreams_LargeMaxConcurrentStreams_DoubleLimit() Assert.Equal((uint)int.MaxValue * 2, _connection.MaxTrackedStreams); await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -1660,7 +1699,8 @@ public void MinimumMaxTrackedStreams() CreateConnection(); // Kestrel always tracks at least 100 streams Assert.Equal(100u, _connection.MaxTrackedStreams); - _connection.Abort(new ConnectionAbortedException()); + _connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); + AssertConnectionEndReason(ConnectionEndReason.AbortedByApp); } [Fact] @@ -1727,8 +1767,10 @@ public async Task AbortConnectionAfterTooManyEnhanceYourCalms() await WaitForConnectionErrorAsync( ignoreNonGoAwayFrames: true, expectedLastStreamId: int.MaxValue, - expectedErrorCode: Http2ErrorCode.INTERNAL_ERROR, + expectedErrorCode: Http2ErrorCode.ENHANCE_YOUR_CALM, expectedErrorMessage: CoreStrings.Http2ConnectionFaulted); + + AssertConnectionEndReason(ConnectionEndReason.StreamResetLimitExceeded); } private async Task RequestUntilEnhanceYourCalm(int maxStreamsPerConnection, int sentStreams) @@ -1758,6 +1800,7 @@ private async Task RequestUntilEnhanceYourCalm(int maxStreamsPerConnection, int tcs.SetResult(); await StopConnectionAsync(streamId, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -1786,6 +1829,8 @@ public async Task DATA_Received_StreamClosedImplicitly_ConnectionError() expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); } [Fact] @@ -1808,6 +1853,8 @@ public async Task DATA_Received_NoStreamWindowSpace_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorFlowControlWindowExceeded); + + AssertConnectionEndReason(ConnectionEndReason.FlowControlWindowExceeded); } [Fact] @@ -1837,6 +1884,8 @@ public async Task DATA_Received_NoConnectionWindowSpace_ConnectionError() expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorFlowControlWindowExceeded); + + AssertConnectionEndReason(ConnectionEndReason.FlowControlWindowExceeded); } [Fact] @@ -1918,6 +1967,8 @@ public async Task DATA_Sent_DespiteConnectionOutputFlowControl_IfEmptyAndEndsStr await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); await WaitForAllStreamsAsync(); + + AssertConnectionNoError(); } [Fact] @@ -2002,6 +2053,8 @@ public async Task DATA_Sent_DespiteStreamOutputFlowControl_IfEmptyAndEndsStream( withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2054,6 +2107,8 @@ public async Task StreamWindow_BiggerThan_ConnectionWindow() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2071,6 +2126,8 @@ public async Task HEADERS_Received_Decoded() VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2091,6 +2148,8 @@ public async Task HEADERS_Received_WithPadding_Decoded(byte padLength) VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2108,6 +2167,8 @@ public async Task HEADERS_Received_WithPriority_Decoded() VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2128,6 +2189,8 @@ public async Task HEADERS_Received_WithPriorityAndPadding_Decoded(byte padLength VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2174,6 +2237,8 @@ public async Task HEADERS_Received_WithTrailers_Available(bool sendData) } await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2206,6 +2271,8 @@ public async Task HEADERS_Received_ContainsExpect100Continue_100ContinueSent() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2255,6 +2322,8 @@ public async Task HEADERS_Received_AppCannotBlockOtherFrames() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2289,6 +2358,8 @@ public async Task HEADERS_HeaderTableSizeLimitZero_Received_DynamicTableUpdate() withStreamId: 3); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2447,6 +2518,8 @@ public async Task HEADERS_Received_StreamIdZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.HEADERS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2461,6 +2534,8 @@ public async Task HEADERS_Received_StreamIdEven_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.HEADERS, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2487,6 +2562,8 @@ public async Task HEADERS_Received_StreamClosed_ConnectionError() CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1) }); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2504,6 +2581,8 @@ public async Task HEADERS_Received_StreamHalfClosedRemote_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); } [Fact] @@ -2526,6 +2605,8 @@ public async Task HEADERS_Received_StreamClosedImplicitly_ConnectionError() expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -2543,6 +2624,8 @@ public async Task HEADERS_Received_PaddingEqualToFramePayloadLength_ConnectionEr expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.HEADERS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -2557,6 +2640,8 @@ public async Task HEADERS_Received_PaddingFieldMissing_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.HEADERS, expectedLength: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Theory] @@ -2573,6 +2658,8 @@ public async Task HEADERS_Received_PaddingGreaterThanFramePayloadLength_Connecti expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.HEADERS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -2588,6 +2675,8 @@ public async Task HEADERS_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.HEADERS, streamId: 3, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -2602,6 +2691,8 @@ public async Task HEADERS_Received_WithPriority_StreamDependencyOnSelf_Connectio expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamSelfDependency(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.StreamSelfDependency); } [Fact] @@ -2616,6 +2707,8 @@ public async Task HEADERS_Received_IncompleteHeaderBlock_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, expectedErrorMessage: SR.net_http_hpack_incomplete_header_block); + + AssertConnectionEndReason(ConnectionEndReason.ErrorReadingHeaders); } [Fact] @@ -2644,6 +2737,8 @@ public async Task HEADERS_Received_IntegerOverLimit_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, expectedErrorMessage: SR.net_http_hpack_bad_integer); + + AssertConnectionEndReason(ConnectionEndReason.ErrorReadingHeaders); } [Theory] @@ -2660,6 +2755,8 @@ public async Task HEADERS_Received_WithTrailers_ContainsIllegalTrailer_Connectio expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: expectedErrorMessage); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -2678,6 +2775,8 @@ public async Task HEADERS_Received_WithTrailers_EndStreamNotSet_ConnectionError( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream); + + AssertConnectionEndReason(ConnectionEndReason.MissingStreamEnd); } [Theory] @@ -2692,6 +2791,8 @@ public async Task HEADERS_Received_HeaderNameContainsUpperCaseCharacter_Connecti expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.HttpErrorHeaderNameUppercase); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2743,6 +2844,8 @@ public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeade withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2761,6 +2864,8 @@ private async Task HEADERS_Received_InvalidHeaderFields_ConnectionError(IEnumera expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: expectedErrorMessage); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -2782,6 +2887,8 @@ public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeade expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2896,6 +3003,8 @@ public async Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsTrailers_N withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2914,6 +3023,8 @@ public async Task HEADERS_Received_RequestLineLength_StreamError() await WaitForStreamErrorAsync(1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.BadRequest_RequestLineTooLong); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2955,6 +3066,8 @@ public async Task PRIORITY_Received_StreamIdZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.PRIORITY)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2969,6 +3082,8 @@ public async Task PRIORITY_Received_StreamIdEven_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.PRIORITY, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -2985,6 +3100,8 @@ public async Task PRIORITY_Received_LengthNotFive_ConnectionError(int length) expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.PRIORITY, expectedLength: 5)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3000,6 +3117,8 @@ public async Task PRIORITY_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.PRIORITY, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -3014,6 +3133,8 @@ public async Task PRIORITY_Received_StreamDependencyOnSelf_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamSelfDependency(Http2FrameType.PRIORITY, 1)); + + AssertConnectionEndReason(ConnectionEndReason.StreamSelfDependency); } [Fact] @@ -3134,6 +3255,8 @@ public async Task RST_STREAM_Received_ContinuesAppsAwaitingConnectionOutputFlowC await WaitForAllStreamsAsync(); Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -3218,6 +3341,8 @@ async Task VerifyStreamBackpressure(int streamId, int headersLength) Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -3266,6 +3391,8 @@ public async Task RST_STREAM_Received_StreamIdZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.RST_STREAM)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -3280,6 +3407,8 @@ public async Task RST_STREAM_Received_StreamIdEven_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.RST_STREAM, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -3294,6 +3423,8 @@ public async Task RST_STREAM_Received_StreamIdle_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdle(Http2FrameType.RST_STREAM, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -3313,6 +3444,8 @@ public async Task RST_STREAM_Received_LengthNotFour_ConnectionError(int length) expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.RST_STREAM, expectedLength: 4)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3328,6 +3461,8 @@ public async Task RST_STREAM_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.RST_STREAM, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } // Compare to h2spec http2/5.1/8 @@ -3353,6 +3488,8 @@ public async Task RST_STREAM_IncompleteRequest_AdditionalDataFrames_ConnectionAb await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.STREAM_CLOSED, CoreStrings.FormatHttp2ErrorStreamAborted(Http2FrameType.DATA, 1)); + + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); } [Fact] @@ -3377,6 +3514,8 @@ public async Task RST_STREAM_IncompleteRequest_AdditionalTrailerFrames_Connectio await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.STREAM_CLOSED, CoreStrings.FormatHttp2ErrorStreamAborted(Http2FrameType.HEADERS, 1)); + + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); } [Fact] @@ -3399,6 +3538,8 @@ public async Task RST_STREAM_IncompleteRequest_AdditionalResetFrame_IgnoreAdditi tcs.TrySetResult(); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/53744")] @@ -3473,6 +3614,8 @@ public async Task SETTINGS_KestrelDefaults_Sent() withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3532,6 +3675,8 @@ public async Task SETTINGS_Custom_Sent() withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3567,6 +3712,8 @@ public async Task SETTINGS_Received_StreamIdNotZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdNotZero(Http2FrameType.SETTINGS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -3593,6 +3740,8 @@ public async Task SETTINGS_Received_InvalidParameterValue_ConnectionError(int in expectedLastStreamId: 0, expectedErrorCode: expectedErrorCode, expectedErrorMessage: CoreStrings.FormatHttp2ErrorSettingsParameterOutOfRange(parameter)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidSettings); } [Fact] @@ -3608,6 +3757,8 @@ public async Task SETTINGS_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.SETTINGS, streamId: 0, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Theory] @@ -3624,6 +3775,8 @@ public async Task SETTINGS_Received_WithACK_LengthNotZero_ConnectionError(int le expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorSettingsAckLengthNotZero); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Theory] @@ -3643,6 +3796,8 @@ public async Task SETTINGS_Received_LengthNotMultipleOfSix_ConnectionError(int l expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorSettingsLengthNotMultipleOfSix); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3667,6 +3822,8 @@ public async Task SETTINGS_Received_WithInitialWindowSizePushingStreamWindowOver expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorInitialWindowSizeInvalid); + + AssertConnectionEndReason(ConnectionEndReason.InvalidSettings); } [Fact] @@ -3724,6 +3881,8 @@ public async Task SETTINGS_Received_ChangesAllowedResponseMaxFrameSize() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3763,6 +3922,8 @@ public async Task SETTINGS_Received_ClientMaxFrameSizeCannotExceedServerMaxFrame withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3844,6 +4005,8 @@ public async Task PUSH_PROMISE_Received_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorPushPromiseReceived); + + AssertConnectionEndReason(ConnectionEndReason.UnsupportedFrame); } [Fact] @@ -3883,6 +4046,8 @@ public async Task PING_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.PING, streamId: 0, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -3897,6 +4062,8 @@ public async Task PING_Received_StreamIdNotZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdNotZero(Http2FrameType.PING)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -3915,6 +4082,8 @@ public async Task PING_Received_LengthNotEight_ConnectionError(int length) expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.PING, expectedLength: 8)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3925,6 +4094,8 @@ public async Task GOAWAY_Received_ConnectionStops() await SendGoAwayAsync(); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3938,6 +4109,8 @@ public async Task GOAWAY_Received_ConnectionLifetimeNotification_Cancelled() Assert.True(lifetime.ConnectionClosedRequested.IsCancellationRequested); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3983,6 +4156,8 @@ public async Task GOAWAY_Received_SetsConnectionStateToClosingAndWaitForAllStrea TriggerTick(); await WaitForConnectionStopAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); await _closedStateReached.Task.DefaultTimeout(); + + AssertConnectionNoError(); } [Fact] @@ -4086,6 +4261,8 @@ public async Task GOAWAY_Received_ContinuesAppsAwaitingConnectionOutputFlowContr Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -4163,6 +4340,8 @@ async Task VerifyStreamBackpressure(int streamId, int headersLength) Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -4177,6 +4356,8 @@ public async Task GOAWAY_Received_StreamIdNotZero_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdNotZero(Http2FrameType.GOAWAY)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4192,6 +4373,8 @@ public async Task GOAWAY_Received_InterleavedWithHeaders_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.GOAWAY, streamId: 0, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -4206,6 +4389,8 @@ public async Task WINDOW_UPDATE_Received_StreamIdEven_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.WINDOW_UPDATE, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4221,6 +4406,8 @@ public async Task WINDOW_UPDATE_Received_InterleavedWithHeaders_ConnectionError( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.WINDOW_UPDATE, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Theory] @@ -4239,6 +4426,8 @@ public async Task WINDOW_UPDATE_Received_LengthNotFour_ConnectionError(int strea expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.WINDOW_UPDATE, expectedLength: 4)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -4253,6 +4442,8 @@ public async Task WINDOW_UPDATE_Received_OnConnection_SizeIncrementZero_Connecti expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateIncrementZero); + + AssertConnectionEndReason(ConnectionEndReason.WindowUpdateSizeInvalid); } [Fact] @@ -4268,6 +4459,8 @@ public async Task WINDOW_UPDATE_Received_OnStream_SizeIncrementZero_ConnectionEr expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateIncrementZero); + + AssertConnectionEndReason(ConnectionEndReason.WindowUpdateSizeInvalid); } [Fact] @@ -4282,6 +4475,8 @@ public async Task WINDOW_UPDATE_Received_StreamIdle_ConnectionError() expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdle(Http2FrameType.WINDOW_UPDATE, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4299,6 +4494,8 @@ public async Task WINDOW_UPDATE_Received_OnConnection_IncreasesWindowAboveMaxVal expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateSizeInvalid); + + AssertConnectionEndReason(ConnectionEndReason.WindowUpdateSizeInvalid); } [Fact] @@ -4318,6 +4515,8 @@ public async Task WINDOW_UPDATE_Received_OnStream_IncreasesWindowAboveMaxValue_S expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateSizeInvalid); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4409,6 +4608,8 @@ public async Task WINDOW_UPDATE_Received_OnConnection_Respected() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4596,6 +4797,8 @@ public async Task CONTINUATION_Received_StreamIdMismatch_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.CONTINUATION, streamId: 3, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -4611,6 +4814,8 @@ public async Task CONTINUATION_Received_IncompleteHeaderBlock_ConnectionError() expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, expectedErrorMessage: SR.net_http_hpack_incomplete_header_block); + + AssertConnectionEndReason(ConnectionEndReason.ErrorReadingHeaders); } [Theory] @@ -4628,6 +4833,8 @@ public async Task CONTINUATION_Received_WithTrailers_ContainsIllegalTrailer_Conn expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: expectedErrorMessage); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -4651,6 +4858,8 @@ public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudo expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -4668,6 +4877,8 @@ public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudo withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4708,6 +4919,8 @@ public async Task CONTINUATION_Sent_WhenHeadersLargerThanFrameLength() Assert.Equal(_4kHeaderValue, _decodedHeaders["f"]); Assert.Equal(_4kHeaderValue, _decodedHeaders["g"]); Assert.Equal(_4kHeaderValue, _decodedHeaders["h"]); + + AssertConnectionNoError(); } [Fact] @@ -4725,6 +4938,8 @@ public async Task UnknownFrameType_Received_Ignored() withStreamId: 0); await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4740,6 +4955,8 @@ public async Task UnknownFrameType_Received_InterleavedWithHeaders_ConnectionErr expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(frameType: 42, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -4765,6 +4982,8 @@ public async Task ConnectionErrorAbortsAllStreams() Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4778,6 +4997,8 @@ public async Task ConnectionResetLoggedWithActiveStreams() await StopConnectionAsync(1, ignoreNonGoAwayFrames: false); Assert.Single(LogMessages, m => m.Exception is ConnectionResetException); + + AssertConnectionEndReason(ConnectionEndReason.ConnectionReset); } [Fact] @@ -4789,6 +5010,8 @@ public async Task ConnectionResetNotLoggedWithNoActiveStreams() await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); Assert.DoesNotContain(LogMessages, m => m.Exception is ConnectionResetException); + + AssertConnectionNoError(); } [Fact] @@ -4802,6 +5025,8 @@ public async Task OnInputOrOutputCompletedCompletesOutput() var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); Assert.True(result.IsCompleted); Assert.True(result.Buffer.IsEmpty); + + AssertConnectionNoError(); } [Fact] @@ -4809,10 +5034,12 @@ public async Task AbortSendsFinalGOAWAY() { await InitializeConnectionAsync(_noopApplication); - _connection.Abort(new ConnectionAbortedException()); + _connection.Abort(new ConnectionAbortedException(), ConnectionEndReason.AbortedByApp); await _closedStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + + AssertConnectionEndReason(ConnectionEndReason.AbortedByApp); } [Fact] @@ -4825,6 +5052,8 @@ public async Task CompletionSendsFinalGOAWAY() await _closedStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), 0, Http2ErrorCode.NO_ERROR); + + AssertConnectionNoError(); } [Fact] @@ -4869,6 +5098,8 @@ public async Task StopProcessingNextRequestSendsGracefulGOAWAYAndWaitsForStreams _pair.Application.Input.CancelPendingRead(); result = await readTask; Assert.True(result.IsCompleted); + + AssertConnectionNoError(); } [Fact] @@ -4878,7 +5109,7 @@ public async Task StopProcessingNextRequestSendsGracefulGOAWAYThenFinalGOAWAYWhe await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - _connection.StopProcessingNextRequest(); + _connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdown); await _closingStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), Int32.MaxValue, Http2ErrorCode.NO_ERROR); @@ -4900,6 +5131,8 @@ public async Task StopProcessingNextRequestSendsGracefulGOAWAYThenFinalGOAWAYWhe TriggerTick(); await _closedStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), 1, Http2ErrorCode.NO_ERROR); + + AssertConnectionEndReason(ConnectionEndReason.AppShutdown); } [Fact] @@ -4910,7 +5143,7 @@ public async Task AcceptNewStreamsDuringClosingConnection() await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - _connection.StopProcessingNextRequest(); + _connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdown); VerifyGoAway(await ReceiveFrameAsync(), Int32.MaxValue, Http2ErrorCode.NO_ERROR); await _closingStateReached.Task.DefaultTimeout(); @@ -4947,6 +5180,8 @@ public async Task AcceptNewStreamsDuringClosingConnection() TriggerTick(); await WaitForConnectionStopAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionEndReason(ConnectionEndReason.AppShutdown); } [Fact] @@ -4967,6 +5202,8 @@ public async Task IgnoreNewStreamsDuringClosedConnection() var result = await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout(); Assert.True(result.IsCompleted); Assert.True(result.Buffer.IsEmpty); + + AssertConnectionEndReason(ConnectionEndReason.ConnectionReset); } [Fact] @@ -4985,6 +5222,8 @@ public async Task IOExceptionDuringFrameProcessingIsNotLoggedHigherThanDebug() Assert.Equal("Connection id \"TestConnectionId\" request processing ended abnormally.", logMessage.Message); Assert.Same(ioException, logMessage.Exception); + + AssertConnectionEndReason(ConnectionEndReason.IOError); } [Fact] @@ -5002,6 +5241,8 @@ public async Task UnexpectedExceptionDuringFrameProcessingLoggedAWarning() Assert.Equal(LogLevel.Warning, logMessage.LogLevel); Assert.Equal(CoreStrings.RequestProcessingEndError, logMessage.Message); Assert.Same(exception, logMessage.Exception); + + AssertConnectionEndReason(ConnectionEndReason.OtherError); } [Theory] @@ -5053,6 +5294,8 @@ public async Task AppDoesNotReadRequestBody_ResetsAndDrainsRequest(int intFinalF } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -5097,6 +5340,8 @@ public async Task AbortedStream_ResetsAndDrainsRequest(int intFinalFrameType) } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -5141,6 +5386,8 @@ public async Task ResetStream_ResetsAndDrainsRequest(int intFinalFrameType) } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -5225,6 +5472,8 @@ async Task CompletePipeOnTaskCompletion() } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -5295,6 +5544,8 @@ async Task CompletePipeOnTaskCompletion() await WaitForStreamErrorAsync(7, Http2ErrorCode.REFUSED_STREAM, "HTTP/2 stream ID 7 error (REFUSED_STREAM): A new stream was refused because this connection has reached its stream limit."); requestBlock.SetResult(); await StopConnectionAsync(expectedLastStreamId: 7, ignoreNonGoAwayFrames: true); + + AssertConnectionNoError(); } [Fact] @@ -5339,6 +5590,7 @@ public async Task FramesInBatchAreStillProcessedAfterStreamError_WithoutHeartbea Assert.Equal(streamPayload, streamResponse); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: true); + AssertConnectionNoError(); } [Theory] @@ -5377,6 +5629,7 @@ public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterEndOfSt CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); break; case Http2FrameType.HEADERS: @@ -5393,6 +5646,7 @@ public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterEndOfSt CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); break; case Http2FrameType.CONTINUATION: @@ -5410,6 +5664,7 @@ public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterEndOfSt CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); break; default: throw new NotImplementedException(finalFrameType.ToString()); @@ -5461,6 +5716,20 @@ public async Task AbortedStream_ResetsAndDrainsRequest_RefusesFramesAfterClientR CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, streamId: 1), CoreStrings.FormatHttp2ErrorStreamAborted(finalFrameType, streamId: 1) }); + + switch (finalFrameType) + { + case Http2FrameType.DATA: + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); + break; + + case Http2FrameType.HEADERS: + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); + break; + + default: + throw new NotImplementedException(finalFrameType.ToString()); + } } [Fact] @@ -5476,6 +5745,7 @@ public async Task StartConnection_SendPreface_ReturnSettings() withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: true); + AssertConnectionNoError(); } [Fact] @@ -5489,6 +5759,8 @@ public async Task StartConnection_SendHttp1xRequest_ReturnHttp11Status400() Assert.NotNull(Http2Connection.InvalidHttp1xErrorResponseBytes); Assert.Equal(Http2Connection.InvalidHttp1xErrorResponseBytes, data); + AssertConnectionEndReason(ConnectionEndReason.InvalidHttpVersion); + Assert.Equal(Http2ErrorCode.PROTOCOL_ERROR, (Http2ErrorCode)_errorCodeFeature.Error); } [Fact] @@ -5503,6 +5775,7 @@ public async Task StartConnection_SendHttp1xRequest_ExceedsRequestLineLimit_Prot expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorInvalidPreface); + AssertConnectionEndReason(ConnectionEndReason.InvalidHandshake); } [Fact] @@ -5519,6 +5792,7 @@ public async Task StartTlsConnection_SendHttp1xRequest_NoError() await SendAsync(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\n")); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -5527,6 +5801,7 @@ public async Task StartConnection_SendNothing_NoError() InitializeConnectionWithoutPreface(_noopApplication); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } public static TheoryData UpperCaseHeaderNameData diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs index ff6863aa325f..b71387129a10 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2KeepAliveTests.cs @@ -18,6 +18,7 @@ public async Task KeepAlivePingDelay_InfiniteTimeSpan_KeepAliveNotEnabled() Assert.Null(_connection._keepAlive); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -47,6 +48,7 @@ public async Task KeepAlivePingTimeout_InfiniteTimeSpan_NoGoAway() Assert.Equal(KeepAliveState.PingSent, _connection._keepAlive._state); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -68,6 +70,7 @@ public async Task IntervalExceeded_WithoutActivity_PingSent() withStreamId: 0).DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -90,6 +93,7 @@ public async Task IntervalExceeded_WithActivity_NoPingSent() TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -106,6 +110,7 @@ public async Task IntervalNotExceeded_NoPingSent() TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -128,6 +133,7 @@ public async Task IntervalExceeded_MultipleTimes_PingsNotSentWhileAwaitingOnAck( TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -165,6 +171,7 @@ public async Task IntervalExceeded_MultipleTimes_PingSentAfterAck() withStreamId: 0).DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -195,6 +202,8 @@ public async Task TimeoutExceeded_NoAck_GoAway() Assert.Equal(KeepAliveState.Timeout, _connection._keepAlive._state); VerifyGoAway(await ReceiveFrameAsync().DefaultTimeout(), 0, Http2ErrorCode.INTERNAL_ERROR); + + AssertConnectionEndReason(ConnectionEndReason.KeepAliveTimeout); } [Fact] @@ -226,6 +235,7 @@ public async Task TimeoutExceeded_NonPingActivity_NoGoAway() withStreamId: 1).DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -249,6 +259,7 @@ public async Task IntervalExceeded_StreamStarted_NoPingSent() TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -302,6 +313,7 @@ public async Task IntervalExceeded_ConnectionFlowControlUsedUpThenPings_NoPingSe // Server could send RST_STREAM await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -359,5 +371,6 @@ public async Task TimeoutExceeded_ConnectionFlowControlUsedUpThenPings_NoGoAway( // Server could send RST_STREAM await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true).DefaultTimeout(); + AssertConnectionNoError(); } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 8628af4117d9..0429af4e6924 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -37,6 +38,7 @@ public async Task HEADERS_Received_NewLineCharactersInValue_ConnectionError(stri await StartStreamAsync(1, headers, endStream: true); await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, 1, Http2ErrorCode.PROTOCOL_ERROR, "Malformed request: invalid headers."); + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2231,6 +2233,7 @@ public async Task ResponseHeaders_WithInvalidValuesAndCustomEncoder_AbortsConnec await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + AssertConnectionEndReason(ConnectionEndReason.ErrorWritingHeaders); } [Fact] @@ -2581,6 +2584,7 @@ public async Task ResponseTrailers_WithInvalidValuesAndCustomEncoder_AbortsConne Assert.Equal("200", _decodedHeaders[InternalHeaderNames.Status]); await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + AssertConnectionEndReason(ConnectionEndReason.ErrorWritingHeaders); } [Fact] @@ -3774,6 +3778,7 @@ public async Task ResponseHeader_OneMegaByte_SplitsHeaderToContinuationFrames() withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true); + Assert.False(ConnectionTags.ContainsKey(KestrelMetrics.ErrorType), "Non-error reason shouldn't be added to error.type"); } [Fact] @@ -5807,6 +5812,7 @@ public async Task HEADERS_Received_Latin1_RejectedWhenLatin1OptionIsNotConfigure expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.BadRequest_MalformedRequestInvalidHeaders); + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -5826,6 +5832,7 @@ public async Task HEADERS_Received_CustomEncoding_InvalidCharacters_AbortsConnec await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, expectedLastStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR, CoreStrings.BadRequest_MalformedRequestInvalidHeaders); + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index c334ee588dab..bb50cd5e4ff3 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -12,6 +12,7 @@ using System.Reflection; using System.Text; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.InternalTesting; @@ -154,10 +155,16 @@ public class Http2TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable, internal TestServiceContext _serviceContext; internal DuplexPipe.DuplexPipePair _pair; + internal IConnectionMetricsTagsFeature _metricsTagsFeature; + internal IConnectionMetricsContextFeature _metricsContextFeature; + internal IProtocolErrorCodeFeature _errorCodeFeature; internal Http2Connection _connection; protected Task _connectionTask; protected long _bytesReceived; + internal IDictionary ConnectionTags => _metricsTagsFeature.Tags.ToDictionary(t => t.Key, t => t.Value); + internal ConnectionMetricsContext MetricsContext => _metricsContextFeature.MetricsContext; + public Http2TestBase() { _hpackDecoder = new HPackDecoder((int)_clientSettings.HeaderTableSize, MaxRequestHeaderFieldSize); @@ -419,6 +426,16 @@ public override void Dispose() base.Dispose(); } + internal void AssertConnectionNoError() + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, ConnectionTags.Keys); + } + + internal void AssertConnectionEndReason(ConnectionEndReason expectedEndReason) + { + Assert.Equal(expectedEndReason, MetricsContext.ConnectionEndReason); + } + void IHttpStreamHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { var nameStr = name.GetHeaderName(); @@ -457,8 +474,16 @@ protected void CreateConnection() _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); + _metricsTagsFeature = new TestConnectionMetricsTagsFeature(); + _errorCodeFeature = new TestProtocolErrorCodeFeature(); + + var metricsContext = TestContextFactory.CreateMetricsContext(_mockConnectionContext.Object); + _metricsContextFeature = new TestConnectionMetricsContextFeature() { MetricsContext = metricsContext }; + var features = new FeatureCollection(); - features.Set(new TestConnectionMetricsContextFeature()); + features.Set(_metricsContextFeature); + features.Set(_metricsTagsFeature); + features.Set(_errorCodeFeature); _mockConnectionContext.Setup(x => x.Features).Returns(features); var httpConnectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: _serviceContext, @@ -466,7 +491,8 @@ protected void CreateConnection() transport: _pair.Transport, memoryPool: _memoryPool, connectionFeatures: features, - timeoutControl: _mockTimeoutControl.Object); + timeoutControl: _mockTimeoutControl.Object, + metricsContext: metricsContext); _connection = new Http2Connection(httpConnectionContext); _connection._streamLifetimeHandler = new LifetimeHandlerInterceptor(_connection._streamLifetimeHandler, this); @@ -479,9 +505,19 @@ protected void CreateConnection() _timeoutControl.Initialize(); } + private sealed class TestConnectionMetricsTagsFeature : IConnectionMetricsTagsFeature + { + public ICollection> Tags { get; } = new List>(); + } + private class TestConnectionMetricsContextFeature : IConnectionMetricsContextFeature { - public ConnectionMetricsContext MetricsContext { get; } + public ConnectionMetricsContext MetricsContext { get; init; } + } + + private class TestProtocolErrorCodeFeature : IProtocolErrorCodeFeature + { + public long Error { get; set; } = -1; } private class LifetimeHandlerInterceptor : IHttp2StreamLifetimeHandler diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs index 74f390495468..0e10567e68b9 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs @@ -99,6 +99,7 @@ public async Task HEADERS_NotReceivedAfterFirstRequest_WithinKeepAliveTimeout_Cl _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.KeepAlive), Times.Once); await WaitForConnectionStopAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionEndReason(ConnectionEndReason.KeepAliveTimeout); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -193,6 +194,7 @@ public async Task HEADERS_ReceivedWithoutAllCONTINUATIONs_WithinRequestHeadersTi expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, CoreStrings.BadRequest_RequestHeadersTimeout); + AssertConnectionEndReason(ConnectionEndReason.RequestHeadersTimeout); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestHeadersTimeout)), Times.Once); @@ -211,6 +213,7 @@ public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() await SendGoAwayAsync(); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); AdvanceTime(TimeSpan.FromSeconds(_bytesReceived / limits.MinResponseDataRate.BytesPerSecond) + limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5)); @@ -298,6 +301,20 @@ async Task AdvanceClockAndSendFrames() Http2ErrorCode.STREAM_CLOSED, CoreStrings.FormatHttp2ErrorStreamClosed(finalFrameType, 1)); + switch (finalFrameType) + { + case Http2FrameType.DATA: + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); + break; + + case Http2FrameType.CONTINUATION: + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); + break; + + default: + throw new NotImplementedException(finalFrameType.ToString()); + } + closed = true; await sendTask.DefaultTimeout(); @@ -371,6 +388,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsC withStreamId: 1); Assert.True((await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout()).IsCompleted); + AssertConnectionEndReason(ConnectionEndReason.MinResponseDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); @@ -425,6 +443,7 @@ public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsC withStreamId: 1); Assert.True((await _pair.Application.Input.ReadAsync().AsTask().DefaultTimeout()).IsCompleted); + AssertConnectionEndReason(ConnectionEndReason.MinResponseDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); @@ -476,6 +495,7 @@ public async Task DATA_Sent_TooSlowlyDueToFlowControlOnSmallWrite_AbortsConnecti expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinResponseDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); @@ -529,6 +549,7 @@ public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnLargeWrite_AbortsCo expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinResponseDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); @@ -594,6 +615,7 @@ public async Task DATA_Sent_TooSlowlyDueToOutputFlowControlOnMultipleStreams_Abo expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinResponseDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); @@ -640,6 +662,7 @@ public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGraceP expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinRequestBodyDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); @@ -690,6 +713,7 @@ public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTi expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinRequestBodyDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); @@ -756,6 +780,7 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinRequestBodyDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); @@ -823,6 +848,7 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinRequestBodyDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); @@ -877,6 +903,7 @@ public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbo withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); @@ -962,6 +989,7 @@ public async Task DATA_Received_SlowlyDueToConnectionFlowControl_DoesNotAbortCon expectedLastStreamId: int.MaxValue, Http2ErrorCode.INTERNAL_ERROR, null); + AssertConnectionEndReason(ConnectionEndReason.MinRequestBodyDataRate); _mockConnectionContext.Verify(c => c.Abort(It.Is(e => e.Message == CoreStrings.BadRequest_RequestBodyTimeout)), Times.Once); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs index 1dee2193a9d7..c6cd7f77b2c6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/TlsTests.cs @@ -17,6 +17,8 @@ using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.InternalTesting; using Xunit; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.Http2; @@ -33,6 +35,9 @@ public class TlsTests : LoggedTest SkipReason = "Windows versions newer than 20H2 do not enable TLS 1.1: https://github.com/dotnet/aspnetcore/issues/37761")] public async Task TlsHandshakeRejectsTlsLessThan12() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + await using (var server = new TestServer(context => { var tlsFeature = context.Features.Get(); @@ -41,7 +46,7 @@ public async Task TlsHandshakeRejectsTlsLessThan12() return context.Response.WriteAsync("hello world " + context.Request.Protocol); }, - new TestServiceContext(LoggerFactory), + new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)), listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; @@ -71,6 +76,11 @@ public async Task TlsHandshakeRejectsTlsLessThan12() reader.Complete(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.InsufficientTlsVersion), m.Tags[KestrelMetrics.ErrorType]); + }); } private async Task WaitForConnectionErrorAsync(PipeReader reader, bool ignoreNonGoAwayFrames, int expectedLastStreamId, Http2ErrorCode expectedErrorCode) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs index 2595f6dd76c3..5b3fe8d95f9f 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -21,6 +21,7 @@ using Microsoft.Net.Http.Headers; using Xunit; using Http3SettingType = Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3.Http3SettingType; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -190,6 +191,7 @@ public async Task GOAWAY_GracefulServerShutdown_SendsGoAway(int connectionReques Assert.Null(await Http3Api.MultiplexedConnectionContext.AcceptAsync().DefaultTimeout()); await Http3Api.WaitForConnectionStopAsync(expectedStreamId, false, expectedErrorCode: Http3ErrorCode.NoError); + Assert.DoesNotContain(KestrelMetrics.ErrorType, Http3Api.ConnectionTags.Keys); } [Fact] @@ -219,6 +221,7 @@ public async Task GOAWAY_GracefulServerShutdownWithActiveRequest_SendsMultipleGo Http3Api.MultiplexedConnectionContext.Abort(); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + Assert.DoesNotContain(KestrelMetrics.ErrorType, Http3Api.ConnectionTags.Keys); } [Theory] @@ -245,6 +248,7 @@ public async Task SETTINGS_ReservedSettingSent_ConnectionError(long settingIdent expectedErrorCode: Http3ErrorCode.SettingsError, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorControlStreamReservedSetting($"0x{settingIdentifier.ToString("X", CultureInfo.InvariantCulture)}")); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.InvalidSettings), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Theory] @@ -264,6 +268,7 @@ public async Task InboundStreams_CreateMultiple_ConnectionError(int streamId, st expectedErrorCode: Http3ErrorCode.StreamCreationError, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams(name)); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.StreamCreationError), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Theory] @@ -285,6 +290,7 @@ public async Task ControlStream_ClientToServer_UnexpectedFrameType_ConnectionErr expectedErrorCode: Http3ErrorCode.UnexpectedFrame, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(f))); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.UnexpectedFrame), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -309,6 +315,7 @@ public async Task ControlStream_ClientToServer_Completes_ConnectionError() expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.ClosedCriticalStream), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -332,6 +339,7 @@ public async Task GOAWAY_TriggersLifetimeNotification_ConnectionClosedRequested( Http3Api.CloseServerGracefully(); await Http3Api.WaitForConnectionStopAsync(0, true, expectedErrorCode: Http3ErrorCode.NoError); + Assert.DoesNotContain(KestrelMetrics.ErrorType, Http3Api.ConnectionTags.Keys); } [Fact] @@ -352,6 +360,7 @@ public async Task ControlStream_ServerToClient_ErrorInitializing_ConnectionError expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.ClosedCriticalStream), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs index d932f784d932..6a34b984d5d2 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs @@ -1,24 +1,19 @@ // 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.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Net.Http; using System.Runtime.ExceptionServices; using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -782,7 +777,7 @@ public async Task FlushPipeAsync_OnStoppedHttp3Stream_ReturnsFlushResultWithIsCo }, requestHeaders, endStream: true); await requestStream.ExpectReceiveEndOfStream(); - await appTcs.Task; + await appTcs.Task.DefaultTimeout(); } [Fact] @@ -2032,6 +2027,13 @@ public async Task FrameAfterTrailers_UnexpectedFrameError() expectedErrorMessage: CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data))); tcs.SetResult(); + + await Http3Api.WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: false, + expectedLastStreamId: null, + Http3ErrorCode.UnexpectedFrame, + null); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.UnexpectedFrame), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -2107,6 +2109,7 @@ public async Task UnexpectedRequestFrame(string frameType, bool pendingStreamsEn expectedErrorCode: Http3ErrorCode.UnexpectedFrame, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(Http3Formatting.ToFormattedType(f))); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.UnsupportedFrame), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Theory] @@ -2130,6 +2133,13 @@ public async Task UnexpectedServerFrame(string frameType) await requestStream.WaitForStreamErrorAsync( Http3ErrorCode.UnexpectedFrame, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(Http3Formatting.ToFormattedType(f))); + + await Http3Api.WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: false, + expectedLastStreamId: null, + Http3ErrorCode.UnexpectedFrame, + null); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.UnsupportedFrame), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs index 996bd97f24cc..b5d98a588581 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TimeoutTests.cs @@ -28,6 +28,7 @@ public async Task KeepAliveTimeout_ControlStreamNotReceived_ConnectionClosed() Http3Api.AdvanceTime(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.KeepAliveTimeout), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -44,6 +45,7 @@ public async Task KeepAliveTimeout_RequestNotReceived_ConnectionClosed() Http3Api.AdvanceTime(limits.KeepAliveTimeout + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.KeepAliveTimeout), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -72,6 +74,7 @@ public async Task KeepAliveTimeout_AfterRequestComplete_ConnectionClosed() Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.KeepAliveTimeout), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -117,6 +120,7 @@ public async Task KeepAliveTimeout_LongRunningRequest_KeepsConnectionAlive() Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.KeepAliveTimeout), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); } [Fact] @@ -365,6 +369,7 @@ public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGraceP expectedLastStreamId: 4, Http3ErrorCode.InternalError, null); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinRequestBodyDataRate), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -408,6 +413,7 @@ public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection() Http3ErrorCode.InternalError, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinResponseDataRate), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); Assert.Contains(TestSink.Writes, w => w.EventId.Name == "ResponseMinimumDataRateNotSatisfied"); } @@ -561,6 +567,7 @@ public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTi expectedLastStreamId: null, Http3ErrorCode.InternalError, null); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinRequestBodyDataRate), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -615,6 +622,7 @@ public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfter expectedLastStreamId: null, Http3ErrorCode.InternalError, null); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinRequestBodyDataRate), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -670,6 +678,7 @@ public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNon expectedLastStreamId: null, Http3ErrorCode.InternalError, null); + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinRequestBodyDataRate), Http3Api.ConnectionTags[KestrelMetrics.ErrorType]); _mockTimeoutHandler.VerifyNoOtherCalls(); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs index aecd2637f4d3..fd799942e29b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs @@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.AspNetCore.Server.Kestrel.Core; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -22,7 +24,10 @@ public class KeepAliveTimeoutTests : LoggedTest [Fact] public async Task ConnectionClosedWhenKeepAliveTimeoutExpires() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -44,12 +49,20 @@ await using (var server = CreateServer(testContext)) await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.KeepAliveTimeout), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] public async Task ConnectionKeptAliveBetweenRequests() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -72,12 +85,20 @@ await using (var server = CreateServer(testContext)) } } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } [Fact] public async Task ConnectionNotTimedOutWhileRequestBeingSent() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -112,12 +133,20 @@ await using (var server = CreateServer(testContext)) await ReceiveResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } [Fact] private async Task ConnectionNotTimedOutWhileAppIsRunning() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var cts = new CancellationTokenSource(); await using (var server = CreateServer(testContext, longRunningCt: cts.Token)) @@ -152,12 +181,20 @@ await using (var server = CreateServer(testContext, longRunningCt: cts.Token)) await ReceiveResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } [Fact] private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -172,12 +209,20 @@ await using (var server = CreateServer(testContext)) await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.KeepAliveTimeout), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var cts = new CancellationTokenSource(); await using (var server = CreateServer(testContext, upgradeCt: cts.Token)) @@ -210,6 +255,11 @@ await using (var server = CreateServer(testContext, upgradeCt: cts.Token)) await connection.Receive("hello, world"); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys); + }); } private TestServer CreateServer(TestServiceContext context, CancellationToken longRunningCt = default, CancellationToken upgradeCt = default) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs index ba2679a09260..27e93a47d69e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KestrelMetricsTests.cs @@ -17,6 +17,10 @@ using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using System.Buffers; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -24,6 +28,16 @@ public class KestrelMetricsTests : TestApplicationErrorLoggerLoggedTest { private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate(); + [Fact] + public void ConnectionEndReasonMappings() + { + foreach (var reason in Enum.GetValues()) + { + var hasValue = KestrelMetrics.TryGetErrorType(reason, out var value); + Assert.True(hasValue || value == null, $"ConnectionEndReason '{reason}' doesn't have a mapping."); + } + } + [Fact] public async Task Http1Connection() { @@ -144,6 +158,157 @@ public async Task Http1Connection_BeginListeningAfterConnectionStarted() } } + [Fact] + public async Task Http1Connection_RequestEndsWithIncompleteReadAsync() + { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); + + var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?"; + + await using var server = new TestServer(async context => + { + var result = await context.Request.BodyReader.ReadAsync(); + await context.Response.BodyWriter.WriteAsync(result.Buffer.ToArray()); + // No BodyReader.Advance. Connection will fail when attempting to complete body. + }, serviceContext); + + using (var connection = server.CreateConnection()) + { + await connection.Send(sendString); + + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {serviceContext.DateHeaderValue}", + "", + "Hello World?"); + + await connection.WaitForConnectionClose(); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http11, error: KestrelMetrics.GetErrorType(ConnectionEndReason.BodyReaderInvalidState)); + }); + } + } + + [Fact] + public async Task Http1Connection_ServerShutdown_Graceful() + { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) + { + ShutdownTimeout = TimeSpan.FromSeconds(60) + }; + + var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?"; + + var getNotificationFeatureTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new TestServer(async c => + { + getNotificationFeatureTcs.TrySetResult(c.Features.Get()); + await EchoApp(c); + }, serviceContext); + using var connection = server.CreateConnection(); + + try + { + await connection.Send(sendString); + } + finally + { + Logger.LogInformation("Waiting for notification feature"); + var notificationFeature = await getNotificationFeatureTcs.Task.DefaultTimeout(); + + // Dispose while the connection is in-progress. + var shutdownTask = server.DisposeAsync(); + + var waitForConnectionCloseRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + notificationFeature.ConnectionClosedRequested.Register(() => + { + Logger.LogInformation("ConnectionClosedRequested"); + waitForConnectionCloseRequest.TrySetResult(); + }); + + Logger.LogInformation("Waiting for connection close request."); + await waitForConnectionCloseRequest.Task.DefaultTimeout(); + + Logger.LogInformation("Receiving data and closing connection."); + await connection.ReceiveEnd( + "HTTP/1.1 200 OK", + "Connection: close", + $"Date: {serviceContext.DateHeaderValue}", + "", + "Hello World?"); + await connection.WaitForConnectionClose(); + connection.Dispose(); + + Logger.LogInformation("Finishing shutting down."); + await shutdownTask; + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http11); + }); + } + + [Fact] + public async Task Http1Connection_ServerShutdown_Abort() + { + ThrowOnUngracefulShutdown = false; + + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) + { + MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool, + ShutdownTimeout = TimeSpan.Zero + }; + + var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?"; + var connectionCloseTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var requestReceivedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new TestServer(async c => + { + requestReceivedTcs.TrySetResult(); + await c.Response.BodyWriter.WriteAsync(Encoding.UTF8.GetBytes("Hello world")); + await c.Response.BodyWriter.FlushAsync(); + await connectionCloseTcs.Task; + Logger.LogInformation("Server request delegate finishing."); + }, serviceContext); + + using var connection = server.CreateConnection(); + connection.TransportConnection.ConnectionClosed.Register(() => + { + Logger.LogInformation("Connection closed raised."); + connectionCloseTcs.TrySetResult(); + }); + + try + { + await connection.Send(sendString); + await requestReceivedTcs.Task.DefaultTimeout(); + } + finally + { + // Dispose while the connection is in-progress. + Logger.LogInformation("Shutting down server."); + await server.DisposeAsync(); + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http11, error: KestrelMetrics.GetErrorType(ConnectionEndReason.AppShutdown)); + }); + } + [Fact] public async Task Http1Connection_IHttpConnectionTagsFeatureIgnoreFeatureSetOnTransport() { @@ -219,6 +384,37 @@ public async Task Http1Connection_IHttpConnectionTagsFeatureIgnoreFeatureSetOnTr Assert.Collection(queuedConnections.GetMeasurementSnapshot(), m => AssertCount(m, 1, "127.0.0.1", localPort: 0, "tcp", "ipv4"), m => AssertCount(m, -1, "127.0.0.1", localPort: 0, "tcp", "ipv4")); } + [Fact] + public async Task Http1Connection_ServerAbort_HasErrorType() + { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); + + var sendString = "POST / HTTP/1.0\r\nContent-Length: 12\r\n\r\nHello World?"; + + await using var server = new TestServer(c => + { + c.Abort(); + return Task.CompletedTask; + }, serviceContext); + + using (var connection = server.CreateConnection()) + { + await connection.Send(sendString).DefaultTimeout(); + + await connection.ReceiveEnd().DefaultTimeout(); + + await connection.WaitForConnectionClose().DefaultTimeout(); + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http11, error: KestrelMetrics.GetErrorType(ConnectionEndReason.AbortedByApp)); + }); + } + private sealed class TestConnectionMetricsTagsFeature : IConnectionMetricsTagsFeature { public ICollection> Tags { get; } = new List>(); @@ -272,8 +468,7 @@ public async Task Http1Connection_Error() Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => { - AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", httpVersion: null); - Assert.Equal("System.InvalidOperationException", (string)m.Tags["error.type"]); + AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", httpVersion: null, error: "System.InvalidOperationException"); }); Assert.Collection(activeConnections.GetMeasurementSnapshot(), m => AssertCount(m, 1, "127.0.0.1", localPort: 0, "tcp", "ipv4"), m => AssertCount(m, -1, "127.0.0.1", localPort: 0, "tcp", "ipv4")); Assert.Collection(queuedConnections.GetMeasurementSnapshot(), m => AssertCount(m, 1, "127.0.0.1", localPort: 0, "tcp", "ipv4"), m => AssertCount(m, -1, "127.0.0.1", localPort: 0, "tcp", "ipv4")); @@ -318,6 +513,143 @@ static async Task UpgradeApp(HttpContext context) } } + [Fact] + public async Task Http2Connection_ServerShutdown_Graceful() + { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var getNotificationFeatureTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new TestServer(async context => + { + getNotificationFeatureTcs.TrySetResult(context.Features.Get()); + await context.Response.BodyWriter.FlushAsync(); + await tcs.Task; + }, + new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) + { + ShutdownTimeout = TimeSpan.FromSeconds(200) + }, + listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + + HttpResponseMessage responseMessage = null; + Stream responseStream = null; + using var connection = server.CreateConnection(); + using var socketsHandler = new SocketsHttpHandler() + { + ConnectCallback = (_, _) => + { + return new ValueTask(connection.Stream); + } + }; + using var httpClient = new HttpClient(socketsHandler); + + try + { + + var httpRequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri("http://localhost/"), + Version = new Version(2, 0), + VersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + responseMessage = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + responseMessage.EnsureSuccessStatusCode(); + responseStream = await responseMessage.Content.ReadAsStreamAsync(); + } + finally + { + var notificationFeature = await getNotificationFeatureTcs.Task.DefaultTimeout(); + + var shutdownTask = server.DisposeAsync(); + + var waitForConnectionCloseRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + notificationFeature.ConnectionClosedRequested.Register(() => + { + waitForConnectionCloseRequest.TrySetResult(); + }); + + await waitForConnectionCloseRequest.Task.DefaultTimeout(); + tcs.TrySetResult(); + + await responseStream.ReadUntilEndAsync().DefaultTimeout(); + responseMessage.Dispose(); + + connection.Dispose(); + + await shutdownTask; + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http2)); + } + + [Fact] + public async Task Http2Connection_ServerShutdown_Abort() + { + ThrowOnUngracefulShutdown = false; + + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) + { + ShutdownTimeout = TimeSpan.Zero, + MemoryPoolFactory = PinnedBlockMemoryPoolFactory.CreatePinnedBlockMemoryPool + }; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new TestServer(async context => + { + await context.Response.BodyWriter.FlushAsync(); + await tcs.Task; + }, + serviceContext, + listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + + HttpResponseMessage responseMessage = null; + using var connection = server.CreateConnection(); + connection.TransportConnection.ConnectionClosed.Register(() => tcs.TrySetResult()); + + using var socketsHandler = new SocketsHttpHandler() + { + ConnectCallback = (_, _) => + { + return new ValueTask(connection.Stream); + } + }; + + using var httpClient = new HttpClient(socketsHandler); + + try + { + var httpRequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri("http://localhost/"), + Version = new Version(2, 0), + VersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + responseMessage = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + responseMessage.EnsureSuccessStatusCode(); + } + finally + { + var shutdownTask = server.DisposeAsync().DefaultTimeout(); + + await shutdownTask; + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http2, error: KestrelMetrics.GetErrorType(ConnectionEndReason.AppShutdown))); + } + [ConditionalFact] [TlsAlpnSupported] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] @@ -405,6 +737,7 @@ public async Task Http2Connection() { Assert.True(m.Value > 0); Assert.Equal("1.2", (string)m.Tags["tls.protocol.version"]); + Assert.DoesNotContain("error.type", m.Tags.Keys); }); Assert.Collection(activeTlsHandshakes.GetMeasurementSnapshot(), m => Assert.Equal(1, m.Value), m => Assert.Equal(-1, m.Value)); @@ -416,6 +749,135 @@ static void AssertRequestCount(CollectedMeasurement measurement, long expe } } + [Fact] + public async Task Http2Connection_ServerAbort_NoErrorType() + { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + await using (var server = new TestServer(context => + { + context.Response.WriteAsync("Hello world"); + context.Abort(); + return Task.CompletedTask; + }, + new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)), + listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + })) + { + using var connection = server.CreateConnection(); + + using var socketsHandler = new SocketsHttpHandler() + { + ConnectCallback = (_, _) => + { + return new ValueTask(connection.Stream); + }, + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (_, _, _, _) => true + } + }; + + using var httpClient = new HttpClient(socketsHandler); + + using var httpRequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri("http://localhost/"), + Version = new Version(2, 0), + VersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + using var responseMessage = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead); + responseMessage.EnsureSuccessStatusCode(); + + await connection.WaitForConnectionClose(); + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", KestrelMetrics.Http2)); + } + + [ConditionalFact] + [TlsAlpnSupported] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)] + public async Task Http2Connection_TlsError() + { + string connectionId = null; + + //const int requestsToSend = 2; + var requestsReceived = 0; + + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + using var tlsHandshakeDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.tls_handshake.duration"); + + await using (var server = new TestServer(context => + { + connectionId = context.Features.Get().ConnectionId; + requestsReceived++; + return Task.CompletedTask; + }, + new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)), + listenOptions => + { + listenOptions.UseHttps(_x509Certificate2, options => + { + options.SslProtocols = SslProtocols.Tls12; + options.ClientCertificateMode = Https.ClientCertificateMode.RequireCertificate; + }); + listenOptions.Protocols = HttpProtocols.Http2; + })) + { + using var connection = server.CreateConnection(); + + using var socketsHandler = new SocketsHttpHandler() + { + ConnectCallback = (_, _) => + { + // This test should only require a single connection. + if (connectionId != null) + { + throw new InvalidOperationException(); + } + + return new ValueTask(connection.Stream); + }, + SslOptions = new SslClientAuthenticationOptions + { + RemoteCertificateValidationCallback = (_, _, _, _) => true + } + }; + + using var httpClient = new HttpClient(socketsHandler); + + //for (int i = 0; i < requestsToSend; i++) + { + using var httpRequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri("https://localhost/"), + Version = new Version(2, 0), + VersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + await Assert.ThrowsAsync(() => httpClient.SendAsync(httpRequestMessage)); + } + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + AssertDuration(m, "127.0.0.1", localPort: 0, "tcp", "ipv4", httpVersion: null, tlsProtocolVersion: null, error: KestrelMetrics.GetErrorType(ConnectionEndReason.TlsHandshakeFailed)); + }); + + Assert.Collection(tlsHandshakeDuration.GetMeasurementSnapshot(), m => + { + Assert.True(m.Value > 0); + Assert.Equal(typeof(AuthenticationException).FullName, (string)m.Tags["error.type"]); + Assert.DoesNotContain("tls.protocol.version", m.Tags.Keys); + }); + } + private static async Task EchoApp(HttpContext httpContext) { var request = httpContext.Request; @@ -429,7 +891,7 @@ private static async Task EchoApp(HttpContext httpContext) } } - private static void AssertDuration(CollectedMeasurement measurement, string localAddress, int? localPort, string networkTransport, string networkType, string httpVersion, string tlsProtocolVersion = null) + private static void AssertDuration(CollectedMeasurement measurement, string localAddress, int? localPort, string networkTransport, string networkType, string httpVersion, string tlsProtocolVersion = null, string error = null) { Assert.True(measurement.Value > 0); Assert.Equal(networkTransport, (string)measurement.Tags["network.transport"]); @@ -440,7 +902,7 @@ private static void AssertDuration(CollectedMeasurement measurement, str } else { - Assert.False(measurement.Tags.ContainsKey("server.port")); + Assert.DoesNotContain("server.port", measurement.Tags.Keys); } if (networkType is not null) { @@ -448,7 +910,7 @@ private static void AssertDuration(CollectedMeasurement measurement, str } else { - Assert.False(measurement.Tags.ContainsKey("network.type")); + Assert.DoesNotContain("network.type", measurement.Tags.Keys); } if (httpVersion is not null) { @@ -457,8 +919,8 @@ private static void AssertDuration(CollectedMeasurement measurement, str } else { - Assert.False(measurement.Tags.ContainsKey("network.protocol.name")); - Assert.False(measurement.Tags.ContainsKey("network.protocol.version")); + Assert.DoesNotContain("network.protocol.name", measurement.Tags.Keys); + Assert.DoesNotContain("network.protocol.version", measurement.Tags.Keys); } if (tlsProtocolVersion is not null) { @@ -466,7 +928,22 @@ private static void AssertDuration(CollectedMeasurement measurement, str } else { - Assert.False(measurement.Tags.ContainsKey("tls.protocol.version")); + Assert.DoesNotContain("tls.protocol.version", measurement.Tags.Keys); + } + if (error is not null) + { + Assert.Equal(error, (string)measurement.Tags["error.type"]); + } + else + { + try + { + Assert.DoesNotContain("error.type", measurement.Tags.Keys); + } + catch (Exception ex) + { + throw new Exception($"Connection has unexpected error.type value: {measurement.Tags["error.type"]}", ex); + } } } @@ -481,7 +958,7 @@ private static void AssertCount(CollectedMeasurement measurement, long exp } else { - Assert.False(measurement.Tags.ContainsKey("server.port")); + Assert.DoesNotContain("server.port", measurement.Tags.Keys); } if (networkType is not null) { @@ -489,7 +966,7 @@ private static void AssertCount(CollectedMeasurement measurement, long exp } else { - Assert.False(measurement.Tags.ContainsKey("network.type")); + Assert.DoesNotContain("network.type", measurement.Tags.Keys); } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs index 2d95a3a20991..09c746201450 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs @@ -12,6 +12,8 @@ using Microsoft.AspNetCore.InternalTesting; using Xunit; using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -107,6 +109,9 @@ public async Task RejectsRequestWithBodySizeExceedingPerRequestLimitAndException [Fact] public async Task RejectsRequestWithChunckedBodySizeExceedingPerRequestLimitAndExceptionWasCaughtByApplication() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var maxRequestBodySize = 3; var customApplicationResponse = "custom"; var chunkedPayload = $"5;random chunk extension\r\nHello\r\n6\r\n World\r\n0\r\n"; @@ -127,7 +132,7 @@ public async Task RejectsRequestWithChunckedBodySizeExceedingPerRequestLimitAndE await context.Response.WriteAsync(customApplicationResponse); throw requestRejectedEx; }, - new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = maxRequestBodySize } } })) + new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ServerOptions = { Limits = { MaxRequestBodySize = maxRequestBodySize } } })) { using var connection = server.CreateConnection(); await connection.Send( @@ -146,6 +151,11 @@ public async Task RejectsRequestWithChunckedBodySizeExceedingPerRequestLimitAndE customApplicationResponse, ""); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MaxRequestBodySizeExceeded), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] @@ -355,6 +365,9 @@ public async Task SettingMaxRequestBodySizeAfterUpgradingRequestThrows() [Fact] public async Task EveryReadFailsWhenContentLengthHeaderExceedsGlobalLimit() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + #pragma warning disable CS0618 // Type or member is obsolete BadHttpRequestException requestRejectedEx1 = null; BadHttpRequestException requestRejectedEx2 = null; @@ -371,7 +384,7 @@ public async Task EveryReadFailsWhenContentLengthHeaderExceedsGlobalLimit() #pragma warning restore CS0618 // Type or member is obsolete throw requestRejectedEx2; }, - new TestServiceContext(LoggerFactory) { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) + new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ServerOptions = { Limits = { MaxRequestBodySize = 0 } } })) { using (var connection = server.CreateConnection()) { @@ -395,6 +408,11 @@ public async Task EveryReadFailsWhenContentLengthHeaderExceedsGlobalLimit() Assert.NotNull(requestRejectedEx2); Assert.Equal(CoreStrings.FormatBadRequest_RequestBodyTooLarge(0), requestRejectedEx1.Message); Assert.Equal(CoreStrings.FormatBadRequest_RequestBodyTooLarge(0), requestRejectedEx2.Message); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MaxRequestBodySizeExceeded), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestLineSizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestLineSizeTests.cs index 3425eb9a2ee7..f3a8e45c1e5b 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestLineSizeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestLineSizeTests.cs @@ -1,13 +1,13 @@ // 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 System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; -using Microsoft.AspNetCore.InternalTesting; -using Microsoft.Extensions.Logging.Testing; -using Xunit; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -28,7 +28,10 @@ public class MaxRequestLineSizeTests : LoggedTest [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\nHost:\r\n\r\n", 1027)] public async Task ServerAcceptsRequestLineWithinLimit(string request, int limit) { - await using (var server = CreateServer(limit)) + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + await using (var server = CreateServer(limit, testMeterFactory)) { using (var connection = server.CreateConnection()) { @@ -45,6 +48,8 @@ await using (var server = CreateServer(limit)) ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys)); } [Theory] @@ -54,7 +59,10 @@ await using (var server = CreateServer(limit)) [InlineData("DELETE /a%20b%20c/d%20e?f=ghi HTTP/1.1\r\n")] public async Task ServerRejectsRequestLineExceedingLimit(string requestLine) { - await using (var server = CreateServer(requestLine.Length - 1)) + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + await using (var server = CreateServer(requestLine.Length - 1, testMeterFactory)) { using (var connection = server.CreateConnection()) { @@ -68,11 +76,16 @@ await using (var server = CreateServer(requestLine.Length - 1)) ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.InvalidRequestLine), m.Tags[KestrelMetrics.ErrorType]); + }); } - private TestServer CreateServer(int maxRequestLineSize) + private TestServer CreateServer(int maxRequestLineSize, IMeterFactory meterFactory) { - return new TestServer(async httpContext => await httpContext.Response.WriteAsync("hello, world"), new TestServiceContext(LoggerFactory) + return new TestServer(async httpContext => await httpContext.Response.WriteAsync("hello, world"), new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(meterFactory)) { ServerOptions = new KestrelServerOptions { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs index 864492a67d6f..55edab87bda7 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestBodyTimeoutTests.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; -using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; @@ -17,8 +19,11 @@ public class RequestBodyTimeoutTests : LoggedTest [Fact] public async Task RequestTimesOutWhenRequestBodyNotReceivedAtSpecifiedMinimumRate() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var gracePeriod = TimeSpan.FromSeconds(5); - var serviceContext = new TestServiceContext(LoggerFactory); + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var appRunningEvent = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -84,13 +89,21 @@ public async Task RequestTimesOutWhenRequestBodyNotReceivedAtSpecifiedMinimumRat ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinRequestBodyDataRate), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] public async Task RequestTimesOutWhenNotDrainedWithinDrainTimeoutPeriod() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); // This test requires a real clock since we can't control when the drain timeout is set - var serviceContext = new TestServiceContext(LoggerFactory); serviceContext.InitializeHeartbeat(); // Ensure there's still a constant date header value. @@ -132,13 +145,21 @@ public async Task RequestTimesOutWhenNotDrainedWithinDrainTimeoutPeriod() Assert.Contains(TestSink.Writes, w => w.EventId.Id == 32 && w.LogLevel == LogLevel.Information); Assert.Contains(TestSink.Writes, w => w.EventId.Id == 33 && w.LogLevel == LogLevel.Information); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.ServerTimeout), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] public async Task ConnectionClosedEvenIfAppSwallowsException() { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var gracePeriod = TimeSpan.FromSeconds(5); - var serviceContext = new TestServiceContext(LoggerFactory); + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var appRunningTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var exceptionSwallowedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -201,5 +222,10 @@ public async Task ConnectionClosedEvenIfAppSwallowsException() "hello, world"); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.MinRequestBodyDataRate), m.Tags[KestrelMetrics.ErrorType]); + }); } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeaderLimitsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeaderLimitsTests.cs index a11eff246a0f..80cd1a3c242e 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeaderLimitsTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeaderLimitsTests.cs @@ -1,13 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Linq; -using System.Threading.Tasks; +using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; -using Microsoft.AspNetCore.InternalTesting; -using Xunit; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -24,9 +24,12 @@ public class RequestHeaderLimitsTests : LoggedTest [InlineData(5, 1337)] public async Task ServerAcceptsRequestWithHeaderTotalSizeWithinLimit(int headerCount, int extraLimit) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var headers = MakeHeaders(headerCount); - await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length + extraLimit)) + await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length + extraLimit, meterFactory: testMeterFactory)) { using (var connection = server.CreateConnection()) { @@ -43,6 +46,8 @@ await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Lengt ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys)); } [Theory] @@ -56,9 +61,12 @@ await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Lengt [InlineData(5, 1337)] public async Task ServerAcceptsRequestWithHeaderCountWithinLimit(int headerCount, int maxHeaderCount) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var headers = MakeHeaders(headerCount); - await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) + await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount, meterFactory: testMeterFactory)) { using (var connection = server.CreateConnection()) { @@ -75,6 +83,8 @@ await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys)); } [Theory] @@ -82,9 +92,12 @@ await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) [InlineData(5)] public async Task ServerRejectsRequestWithHeaderTotalSizeOverLimit(int headerCount) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var headers = MakeHeaders(headerCount); - await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length - 1)) + await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Length - 1, meterFactory: testMeterFactory)) { using (var connection = server.CreateConnection()) { @@ -98,6 +111,11 @@ await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Lengt ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.InvalidRequestHeaders), m.Tags[KestrelMetrics.ErrorType]); + }); } [Theory] @@ -106,9 +124,12 @@ await using (var server = CreateServer(maxRequestHeadersTotalSize: headers.Lengt [InlineData(5, 4)] public async Task ServerRejectsRequestWithHeaderCountOverLimit(int headerCount, int maxHeaderCount) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var headers = MakeHeaders(headerCount); - await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) + await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount, meterFactory: testMeterFactory)) { using (var connection = server.CreateConnection()) { @@ -122,6 +143,11 @@ await using (var server = CreateServer(maxRequestHeaderCount: maxHeaderCount)) ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.InvalidRequestHeaders), m.Tags[KestrelMetrics.ErrorType]); + }); } private static string MakeHeaders(int count) @@ -138,7 +164,7 @@ private static string MakeHeaders(int count) .Select(i => $"Header-{i}: value{i}\r\n"))); } - private TestServer CreateServer(int? maxRequestHeaderCount = null, int? maxRequestHeadersTotalSize = null) + private TestServer CreateServer(int? maxRequestHeaderCount = null, int? maxRequestHeadersTotalSize = null, IMeterFactory meterFactory = null) { var options = new KestrelServerOptions { AddServerHeader = false }; @@ -152,7 +178,8 @@ private TestServer CreateServer(int? maxRequestHeaderCount = null, int? maxReque options.Limits.MaxRequestHeadersTotalSize = maxRequestHeadersTotalSize.Value; } - return new TestServer(async httpContext => await httpContext.Response.WriteAsync("hello, world"), new TestServiceContext(LoggerFactory) + var kestrelMetrics = meterFactory != null ? new KestrelMetrics(meterFactory) : null; + return new TestServer(async httpContext => await httpContext.Response.WriteAsync("hello, world"), new TestServiceContext(LoggerFactory, metrics: kestrelMetrics) { ServerOptions = options }); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs index 965a110ee078..8b0cb40b507d 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeadersTimeoutTests.cs @@ -3,9 +3,11 @@ using System.IO.Pipelines; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport; -using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -21,7 +23,10 @@ public class RequestHeadersTimeoutTests : LoggedTest [InlineData("Host:\r\nContent-Length: 1\r\n\r")] public async Task ConnectionAbortedWhenRequestHeadersNotReceivedInTime(string headers) { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -40,12 +45,20 @@ await using (var server = CreateServer(testContext)) await ReceiveTimeoutResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.RequestHeadersTimeout), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] public async Task RequestHeadersTimeoutCanceledAfterHeadersReceived() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -70,6 +83,8 @@ await using (var server = CreateServer(testContext)) await ReceiveResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => Assert.DoesNotContain(KestrelMetrics.ErrorType, m.Tags.Keys)); } [Theory] @@ -77,7 +92,10 @@ await using (var server = CreateServer(testContext)) [InlineData("POST / HTTP/1.1\r")] public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string requestLine) { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); await using (var server = CreateServer(testContext)) { @@ -94,12 +112,20 @@ await using (var server = CreateServer(testContext)) await ReceiveTimeoutResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.RequestHeadersTimeout), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] public async Task TimeoutNotResetOnEachRequestLineCharacterReceived() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); // Disable response rate, so we can finish the send loop without timing out the response. testContext.ServerOptions.Limits.MinResponseDataRate = null; @@ -123,6 +149,11 @@ await using (var server = CreateServer(testContext)) await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.RequestHeadersTimeout), m.Tags[KestrelMetrics.ErrorType]); + }); } private TestServer CreateServer(TestServiceContext context) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index a983dc03cc03..876a7b4523b5 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Logging; using Moq; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -1051,7 +1052,7 @@ public async Task ContentLengthReadAsyncPipeReaderBufferRequestBody() httpContext.Request.BodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); readResult = await httpContext.Request.BodyReader.ReadAsync(); Assert.Equal(5, readResult.Buffer.Length); - + httpContext.Request.BodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); }, testContext)) { using (var connection = server.CreateConnection()) @@ -1152,7 +1153,10 @@ public async Task ContentLengthReadAsyncPipeReaderReadsCompletedBody() [Fact] public async Task ContentLengthReadAsyncSingleBytesAtATime() { - var testContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var tcs2 = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -1221,6 +1225,11 @@ static async Task ReadAtLeastAsync(PipeReader reader, int numBytes) ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.UnexpectedEndOfRequestContent), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] @@ -2246,6 +2255,39 @@ await using (var server = new TestServer(_ => Task.CompletedTask, testContext)) } } + [Fact] + public async Task TlsOverHttp() + { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var testContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); + + await using (var server = new TestServer(context => + { + return Task.CompletedTask; + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.Stream.WriteAsync(new byte[] { 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0xfc, 0x03, 0x03, 0x03, 0xca, 0xe0, 0xfd, 0x0a }).DefaultTimeout(); + + await connection.ReceiveEnd( + "HTTP/1.1 400 Bad Request", + "Content-Length: 0", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "", + ""); + } + } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.TlsOverHttp), m.Tags[KestrelMetrics.ErrorType]); + }); + } + [Fact] public async Task CustomRequestHeaderEncodingSelectorCanBeConfigured() { diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs index 0909df237f49..915d656b41e1 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ResponseTests.cs @@ -26,6 +26,7 @@ using Moq; using Xunit; using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests; @@ -140,7 +141,10 @@ public async Task OnStartingThrowsWhenSetAfterStartAsyncIsCalled() [Fact] public async Task ResponseBodyWriteAsyncCanBeCancelled() { - var serviceContext = new TestServiceContext(LoggerFactory); + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + + var serviceContext = new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)); var cts = new CancellationTokenSource(); var appTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var writeBlockedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -202,6 +206,11 @@ public async Task ResponseBodyWriteAsyncCanBeCancelled() await Assert.ThrowsAsync(() => appTcs.Task).DefaultTimeout(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(KestrelMetrics.GetErrorType(ConnectionEndReason.WriteCanceled), m.Tags[KestrelMetrics.ErrorType]); + }); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs index 9d0b543a7520..0ee3a41d48ec 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/TestServer.cs @@ -78,7 +78,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action { webHostBuilder - .UseSetting(WebHostDefaults.ShutdownTimeoutKey, TestConstants.DefaultTimeout.TotalSeconds.ToString(CultureInfo.InvariantCulture)) + .UseSetting(WebHostDefaults.ShutdownTimeoutKey, context.ShutdownTimeout.TotalSeconds.ToString(CultureInfo.InvariantCulture)) .Configure(app => { app.Run(_app); }); }) .ConfigureServices(services => diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs index e16404299f90..1b7ae1ff0132 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs @@ -1137,7 +1137,6 @@ public async Task POST_Bidirectional_LargeData_Cancellation_Error(HttpProtocols var badLogWrite = TestSink.Writes.FirstOrDefault(w => w.LogLevel >= LogLevel.Critical); if (badLogWrite != null) { - Debugger.Launch(); Assert.True(false, "Bad log write: " + badLogWrite + Environment.NewLine + badLogWrite.Exception); } @@ -1744,7 +1743,7 @@ public async Task GET_ServerAbortTransport_ConnectionAbortRaised() using (var host = builder.Build()) using (var client = HttpHelpers.CreateClient()) { - await host.StartAsync(); + await host.StartAsync().DefaultTimeout(); var port = host.GetPort(); @@ -1759,7 +1758,7 @@ public async Task GET_ServerAbortTransport_ConnectionAbortRaised() var connection = await connectionStartedTcs.Task.DefaultTimeout(); // Request in progress. - await syncPoint.WaitForSyncPoint(); + await syncPoint.WaitForSyncPoint().DefaultTimeout(); // Server connection middleware triggers close. // Note that this aborts the transport, not the HTTP/3 connection. @@ -1774,7 +1773,7 @@ public async Task GET_ServerAbortTransport_ConnectionAbortRaised() syncPoint.Continue(); - await host.StopAsync(); + await host.StopAsync().DefaultTimeout(); } } diff --git a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj index 2924c934924c..5f03a326d44b 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Shared/Metrics/MetricsExtensions.cs b/src/Shared/Metrics/MetricsExtensions.cs index 307bb9517601..afe9f419a2c7 100644 --- a/src/Shared/Metrics/MetricsExtensions.cs +++ b/src/Shared/Metrics/MetricsExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Http; @@ -12,6 +13,55 @@ public static bool TryAddTag(this IHttpMetricsTagsFeature feature, string name, { var tags = feature.Tags; + return TryAddTagCore(name, value, tags); + } + + public static bool TryAddTag(this IConnectionMetricsTagsFeature feature, string name, object? value) + { + var tags = feature.Tags; + + return TryAddTagCore(name, value, tags); + } + + public static void SetTag(this IConnectionMetricsTagsFeature feature, string name, object? value) + { + var tags = feature.Tags; + + SetTagCore(name, value, tags); + } + + private static void SetTagCore(string name, object? value, ICollection> tags) + { + // Tags is internally represented as a List. + // Prefer looping through the list to avoid allocating an enumerator. + if (tags is List> list) + { + for (var i = 0; i < list.Count; i++) + { + if (list[i].Key == name) + { + list.RemoveAt(i); + break; + } + } + } + else + { + foreach (var tag in tags) + { + if (tag.Key == name) + { + tags.Remove(tag); + break; + } + } + } + + tags.Add(new KeyValuePair(name, value)); + } + + private static bool TryAddTagCore(string name, object? value, ICollection> tags) + { // Tags is internally represented as a List. // Prefer looping through the list to avoid allocating an enumerator. if (tags is List> list) diff --git a/src/Shared/ServerInfrastructure/Http2/ConnectionEndReason.cs b/src/Shared/ServerInfrastructure/Http2/ConnectionEndReason.cs new file mode 100644 index 000000000000..2c7858893bc3 --- /dev/null +++ b/src/Shared/ServerInfrastructure/Http2/ConnectionEndReason.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Server.Kestrel.Core; + +internal enum ConnectionEndReason +{ + Unset, + ConnectionReset, + FlowControlWindowExceeded, + KeepAliveTimeout, + InsufficientTlsVersion, + InvalidHandshake, + InvalidStreamId, + FrameAfterStreamClose, + UnknownStream, + UnsupportedFrame, + UnexpectedFrame, + InvalidFrameLength, + InvalidDataPadding, + InvalidRequestHeaders, + InvalidRequestLine, + StreamResetLimitExceeded, + WindowUpdateSizeInvalid, + StreamSelfDependency, + InvalidSettings, + MissingStreamEnd, + MaxFrameLengthExceeded, + ErrorReadingHeaders, + ErrorWritingHeaders, + OtherError, + InvalidHttpVersion, + RequestHeadersTimeout, + MinRequestBodyDataRate, + MinResponseDataRate, + FlowControlQueueSizeExceeded, + OutputQueueSizeExceeded, + ClosedCriticalStream, + AbortedByApp, + WriteCanceled, + BodyReaderInvalidState, + ServerTimeout, + StreamCreationError, + IOError, + ClientGoAway, + AppShutdown, + GracefulAppShutdown, + TransportCompleted, + TlsHandshakeFailed, + TlsOverHttp, + MaxRequestBodySizeExceeded, + UnexpectedEndOfRequestContent, + MaxConcurrentConnectionsExceeded +} diff --git a/src/Shared/ServerInfrastructure/Http2/Http2ConnectionErrorException.cs b/src/Shared/ServerInfrastructure/Http2/Http2ConnectionErrorException.cs index 19fc06d8d24e..3f4e78c8a5ba 100644 --- a/src/Shared/ServerInfrastructure/Http2/Http2ConnectionErrorException.cs +++ b/src/Shared/ServerInfrastructure/Http2/Http2ConnectionErrorException.cs @@ -7,11 +7,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2; internal sealed class Http2ConnectionErrorException : Exception { - public Http2ConnectionErrorException(string message, Http2ErrorCode errorCode) + public Http2ConnectionErrorException(string message, Http2ErrorCode errorCode, ConnectionEndReason reason) : base($"HTTP/2 connection error ({errorCode}): {message}") { ErrorCode = errorCode; + Reason = reason; } public Http2ErrorCode ErrorCode { get; } + public ConnectionEndReason Reason { get; } } diff --git a/src/Shared/ServerInfrastructure/Http2/Http2FrameReader.cs b/src/Shared/ServerInfrastructure/Http2/Http2FrameReader.cs index f9e9756c95fc..cb02d1a8686f 100644 --- a/src/Shared/ServerInfrastructure/Http2/Http2FrameReader.cs +++ b/src/Shared/ServerInfrastructure/Http2/Http2FrameReader.cs @@ -45,7 +45,7 @@ public static bool TryReadFrame(ref ReadOnlySequence buffer, Http2Frame fr var payloadLength = (int)Bitshifter.ReadUInt24BigEndian(header); if (payloadLength > maxFrameSize) { - throw new Http2ConnectionErrorException(SharedStrings.FormatHttp2ErrorFrameOverLimit(payloadLength, maxFrameSize), Http2ErrorCode.FRAME_SIZE_ERROR); + throw new Http2ConnectionErrorException(SharedStrings.FormatHttp2ErrorFrameOverLimit(payloadLength, maxFrameSize), Http2ErrorCode.FRAME_SIZE_ERROR, ConnectionEndReason.MaxFrameLengthExceeded); } // Make sure the whole frame is buffered @@ -77,7 +77,7 @@ private static int ReadExtendedFields(Http2Frame frame, in ReadOnlySequence frame.PayloadLength) { throw new Http2ConnectionErrorException( - SharedStrings.FormatHttp2ErrorUnexpectedFrameLength(frame.Type, expectedLength: extendedHeaderLength), Http2ErrorCode.FRAME_SIZE_ERROR); + SharedStrings.FormatHttp2ErrorUnexpectedFrameLength(frame.Type, expectedLength: extendedHeaderLength), Http2ErrorCode.FRAME_SIZE_ERROR, ConnectionEndReason.InvalidFrameLength); } var extendedHeaders = readableBuffer.Slice(HeaderLength, extendedHeaderLength).ToSpan();