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 @@ public BaseHttpConnectionContext( IFeatureCollection connectionFeatures, MemoryPool memoryPool, IPEndPoint? localEndPoint, - IPEndPoint? remoteEndPoint) + IPEndPoint? remoteEndPoint, + ConnectionMetricsContext metricsContext) { ConnectionId = connectionId; Protocols = protocols; @@ -31,6 +32,7 @@ public BaseHttpConnectionContext( MemoryPool = memoryPool; LocalEndPoint = localEndPoint; RemoteEndPoint = remoteEndPoint; + MetricsContext = metricsContext; } public string ConnectionId { get; set; } @@ -42,6 +44,7 @@ public 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/Http1ChunkedEncodingMessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs index 78416a213472..5e426ed25721 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO.Pipelines; using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -136,6 +137,7 @@ private async Task PumpAsync() // Read() will have already have greedily consumed the entire request body if able. if (result.IsCompleted) { + KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.UnexpectedEndOfRequestContent); ThrowUnexpectedEndOfRequestContent(); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index 48c9e0595fae..58e040c878f3 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,12 +132,18 @@ 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) { - _keepAlive = false; + DisableKeepAlive(reason); Input.CancelPendingRead(); } + internal override void DisableKeepAlive(ConnectionEndReason reason) + { + KestrelMetrics.AddConnectionEndReason(MetricsContext, reason); + _keepAlive = false; + } + public void SendTimeoutResponse() { _requestTimedOut = true; @@ -142,12 +151,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(); } @@ -606,6 +619,7 @@ internal void EnsureHostHeaderExists() } else if (!HttpUtilities.IsHostHeaderValid(hostText)) { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } @@ -616,6 +630,7 @@ private void ValidateNonOriginHostHeader(string hostText) { if (hostText != RawTarget) { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } @@ -640,6 +655,7 @@ private void ValidateNonOriginHostHeader(string hostText) } else { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } @@ -648,6 +664,7 @@ private void ValidateNonOriginHostHeader(string hostText) if (!HttpUtilities.IsHostHeaderValid(hostText)) { + KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText); } } @@ -707,11 +724,15 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio #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); @@ -758,9 +779,65 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio } } + internal static ConnectionEndReason GetConnectionEndReason(Microsoft.AspNetCore.Http.BadHttpRequestException ex) + { +#pragma warning disable CS0618 // Type or member is obsolete + var kestrelEx = ex as BadHttpRequestException; +#pragma warning restore CS0618 // Type or member is obsolete + + switch (kestrelEx?.Reason) + { + case RequestRejectionReason.UnrecognizedHTTPVersion: + return ConnectionEndReason.InvalidHttpVersion; + case RequestRejectionReason.InvalidRequestLine: + case RequestRejectionReason.RequestLineTooLong: + case RequestRejectionReason.InvalidRequestTarget: + return ConnectionEndReason.InvalidRequestLine; + case RequestRejectionReason.InvalidRequestHeadersNoCRLF: + case RequestRejectionReason.InvalidRequestHeader: + case RequestRejectionReason.InvalidContentLength: + case RequestRejectionReason.MultipleContentLengths: + case RequestRejectionReason.MalformedRequestInvalidHeaders: + case RequestRejectionReason.InvalidCharactersInHeaderName: + case RequestRejectionReason.LengthRequiredHttp10: + case RequestRejectionReason.OptionsMethodRequired: + case RequestRejectionReason.ConnectMethodRequired: + case RequestRejectionReason.MissingHostHeader: + case RequestRejectionReason.MultipleHostHeaders: + case RequestRejectionReason.InvalidHostHeader: + return ConnectionEndReason.InvalidRequestHeaders; + case RequestRejectionReason.HeadersExceedMaxTotalSize: + return ConnectionEndReason.MaxRequestHeadersTotalSizeExceeded; + case RequestRejectionReason.TooManyHeaders: + return ConnectionEndReason.MaxRequestHeaderCountExceeded; + case RequestRejectionReason.TlsOverHttpError: + return ConnectionEndReason.TlsNotSupported; + case RequestRejectionReason.UnexpectedEndOfRequestContent: + return ConnectionEndReason.UnexpectedEndOfRequestContent; + 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. + return ConnectionEndReason.OtherError; + } + } + #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 + { + // 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. + var reason = GetConnectionEndReason(ex); + KestrelMetrics.AddConnectionEndReason(MetricsContext, reason); + + if (ex.Reason == RequestRejectionReason.UnrecognizedHTTPVersion) + { + DetectHttp2Preface(requestData); + } + } + + private void DetectHttp2Preface(ReadOnlySequence requestData) { const int PrefaceLineLength = 16; @@ -770,8 +847,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..90cdf56f506b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs @@ -246,7 +246,7 @@ protected override void OnReadStarting() var maxRequestBodySize = _context.MaxRequestBodySize; if (_contentLength > maxRequestBodySize) { - _context.DisableHttp1KeepAlive(); + _context.DisableKeepAlive(ConnectionEndReason.MaxRequestBodySizeExceeded); KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)); } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index dbdf2a3f5fc2..d14c25e49910 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.InvalidBodyReaderState); 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.InvalidBodyReaderState); } finally { @@ -116,6 +115,12 @@ protected async Task OnConsumeAsyncAwaited() } } + protected override void OnObservedBytesExceedMaxRequestBodySize(long maxRequestBodySize) + { + _context.DisableKeepAlive(ConnectionEndReason.MaxRequestBodySizeExceeded); + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.ToString(CultureInfo.InvariantCulture)); + } + public static MessageBody For( HttpVersion httpVersion, HttpRequestHeaders headers, @@ -202,6 +207,7 @@ public static MessageBody For( // Reject with Length Required for HTTP 1.0. if (httpVersion == HttpVersion.Http10 && (context.Method == HttpMethod.Post || context.Method == HttpMethod.Put)) { + KestrelMetrics.AddConnectionEndReason(context.MetricsContext, ConnectionEndReason.InvalidRequestHeaders); KestrelBadHttpRequestException.Throw(RequestRejectionReason.LengthRequiredHttp10, context.Method); } @@ -221,6 +227,9 @@ protected void ThrowIfReaderCompleted() [StackTraceHidden] protected void ThrowUnexpectedEndOfRequestContent() { + // Set before calling OnInputOrOutputCompleted. + KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.UnexpectedEndOfRequestContent); + // OnInputOrOutputCompleted() is an idempotent method that closes the connection. Sometimes // input completion is observed here before the Input.OnWriterCompleted() callback is fired, // so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400 diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs index cdd2b23c0ca5..a1778a1cb4b5 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; @@ -75,6 +76,7 @@ public Http1OutputProducer( KestrelTrace log, ITimeoutControl timeoutControl, IHttpMinResponseDataRateFeature minResponseDataRateFeature, + ConnectionMetricsContext connectionMetricsContext, IHttpOutputAborter outputAborter) { // Allow appending more data to the PipeWriter when a flush is pending. @@ -84,6 +86,7 @@ public Http1OutputProducer( _memoryPool = memoryPool; _log = log; _minResponseDataRateFeature = minResponseDataRateFeature; + _connectionMetricsContext = connectionMetricsContext; _outputAborter = outputAborter; _flusher = new TimingPipeFlusher(timeoutControl, log); @@ -455,7 +458,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. @@ -466,6 +469,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/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 9bb109efda4e..26f8cdf12016 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -659,7 +659,7 @@ private async Task ProcessRequests(IHttpApplication applicat var messageBody = CreateMessageBody(); if (!messageBody.RequestKeepAlive) { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.RequestNoKeepAlive); } IsUpgradableRequest = messageBody.RequestUpgrade; @@ -860,7 +860,7 @@ private void VerifyAndUpdateWrite(int count) responseHeaders.ContentLength.HasValue && _responseBytesWritten + count > responseHeaders.ContentLength.Value) { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ResponseContentLengthMismatch); ThrowTooManyBytesWritten(count); } @@ -913,7 +913,7 @@ protected bool VerifyResponseContentLength([NotNullWhen(false)] out Exception? e // cannot be certain of how many bytes it will receive. if (_responseBytesWritten > 0) { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ResponseContentLengthMismatch); } ex = new InvalidOperationException( @@ -1032,7 +1032,7 @@ protected Task ProduceEnd() if (HasResponseStarted) { // We can no longer change the response, so we simply close the connection. - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ErrorAfterStartingResponse); OnErrorAfterResponseStarted(); return Task.CompletedTask; } @@ -1139,7 +1139,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) hasConnection && (HttpHeaders.ParseConnection(responseHeaders) & ConnectionOptions.KeepAlive) == 0) { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ResponseNoKeepAlive); } // https://tools.ietf.org/html/rfc7230#section-3.3.1 @@ -1150,7 +1150,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) if (hasTransferEncoding && HttpHeaders.GetFinalTransferCoding(responseHeaders.HeaderTransferEncoding) != TransferCoding.Chunked) { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ResponseNoKeepAlive); } // Set whether response can have body @@ -1186,7 +1186,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) } else if (StatusCode == StatusCodes.Status101SwitchingProtocols) { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ResponseNoKeepAlive); } else if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue) { @@ -1216,7 +1216,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted) } else { - _keepAlive = false; + DisableKeepAlive(ConnectionEndReason.ResponseNoKeepAlive); } } @@ -1392,16 +1392,6 @@ private BadHttpRequestException GetInvalidRequestTargetException(ReadOnlySpan maxRequestBodySize) { - _context.DisableHttp1KeepAlive(); - KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)); + OnObservedBytesExceedMaxRequestBodySize(maxRequestBodySize.Value); } } + protected virtual void OnObservedBytesExceedMaxRequestBodySize(long maxRequestBodySize) + { + KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.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..1870f24f5e80 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -87,6 +87,7 @@ private static int GetMaximumEnhanceYourCalmCount() private readonly HttpConnectionContext _context; private readonly ConnectionMetricsContext _metricsContext; + private readonly IConnectionMetricsTagsFeature? _metricsTagsFeature; private readonly Http2FrameWriter _frameWriter; private readonly Pipe _input; private readonly Task _inputTask; @@ -123,6 +124,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 +140,8 @@ public Http2Connection(HttpConnectionContext context) _context = context; _streamLifetimeHandler = this; - _metricsContext = context.ConnectionFeatures.GetRequiredFeature().MetricsContext; + _metricsContext = context.MetricsContext; + _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 +207,45 @@ 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."); + + 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 +253,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 +271,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl { Exception? error = null; var errorCode = Http2ErrorCode.NO_ERROR; + var reason = ConnectionEndReason.Unset; try { @@ -260,6 +282,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl if (!await TryReadPrefaceAsync()) { + reason = ConnectionEndReason.TransportCompleted; return; } @@ -319,6 +342,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl if (result.IsCompleted) { + reason = ConnectionEndReason.TransportCompleted; return; } @@ -335,7 +359,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl { // 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 +377,11 @@ public async Task ProcessRequestsAsync(IHttpApplication appl if (_clientActiveStreamCount > 0) { Log.RequestProcessingError(ConnectionId, ex); + reason = ConnectionEndReason.ConnectionReset; + } + else + { + reason = ConnectionEndReason.TransportCompleted; } error = ex; @@ -361,6 +390,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl { Log.RequestProcessingError(ConnectionId, ex); error = ex; + reason = ConnectionEndReason.IOError; } catch (ConnectionAbortedException ex) { @@ -372,6 +402,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl Log.Http2ConnectionError(ConnectionId, ex); error = ex; errorCode = ex.ErrorCode; + reason = ex.Reason; } catch (HPackDecodingException ex) { @@ -380,12 +411,14 @@ public async Task ProcessRequestsAsync(IHttpApplication appl 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 +429,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl { if (TryClose()) { + SetConnectionErrorCode(reason, errorCode); await _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, errorCode); } @@ -460,7 +494,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 +577,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 +594,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 +666,7 @@ private Task ProcessFrameAsync(IHttpApplication application, // 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 +676,7 @@ private Task ProcessFrameAsync(IHttpApplication application, 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.UnexpectedFrame), Http2FrameType.PING => ProcessPingFrameAsync(payload), Http2FrameType.GOAWAY => ProcessGoAwayFrameAsync(), Http2FrameType.WINDOW_UPDATE => ProcessWindowUpdateFrameAsync(), @@ -652,17 +689,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 +709,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 +721,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 +738,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 expectedLength) + { + return new Http2ConnectionErrorException(CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(_incomingFrame.Type, expectedLength), 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 +794,7 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli 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 +806,13 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli // (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 +831,7 @@ private Task ProcessHeadersFrameAsync(IHttpApplication appli // // 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 +899,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(expectedLength: 5); } return Task.CompletedTask; @@ -861,17 +924,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(expectedLength: 4); } ThrowIfIncomingFrameSentToIdleStream(); @@ -901,19 +964,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 +984,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 +1018,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 +1036,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 +1048,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(expectedLength: 8); } // Incoming ping resets connection keep alive timeout @@ -1015,16 +1080,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 +1099,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(expectedLength: 4); } ThrowIfIncomingFrameSentToIdleStream(); @@ -1061,14 +1126,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.InvalidWindowUpdateSize); } 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.InvalidWindowUpdateSize); } } else if (_streams.TryGetValue(_incomingFrame.StreamId, out var stream)) @@ -1076,7 +1141,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 +1163,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 +1192,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 +1301,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 +1362,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 +1418,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 +1496,7 @@ private void UpdateConnectionState() { if (TryClose()) { + SetConnectionErrorCode(_gracefulCloseReason, Http2ErrorCode.NO_ERROR); _frameWriter.WriteGoAwayAsync(_highestOpenedStreamId, Http2ErrorCode.NO_ERROR).Preserve(); } } @@ -1496,7 +1562,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.MaxRequestHeadersTotalSizeExceeded); } try @@ -1555,13 +1621,19 @@ private void OnHeaderCore(HeaderType headerType, int? staticTableIndex, ReadOnly } } } +#pragma warning disable CS0618 // Type or member is obsolete + catch (BadHttpRequestException bre) when (bre.Reason == RequestRejectionReason.TooManyHeaders) + { + throw new Http2ConnectionErrorException(bre.Message, Http2ErrorCode.PROTOCOL_ERROR, ConnectionEndReason.MaxRequestHeaderCountExceeded); + } +#pragma warning restore CS0618 // Type or member is obsolete 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 +1644,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 +1655,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 +1688,13 @@ implementations to these vulnerabilities.*/ // 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 +1703,21 @@ implementations to these vulnerabilities.*/ { // 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..685d178a3d03 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.InvalidWindowUpdateSize; + 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 7436784464a0..fa0e07573524 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs @@ -239,9 +239,9 @@ 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) + // 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, 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 @@ public async Task ProcessRequestsAsync(IHttpApplication appl Http3ControlStream? outboundControlStream = null; ValueTask outboundControlStreamTask = default; bool clientAbort = false; + ConnectionEndReason reason = ConnectionEndReason.Unset; try { @@ -459,6 +463,7 @@ public async Task ProcessRequestsAsync(IHttpApplication appl if (_activeRequestCount > 0) { Log.RequestProcessingError(_context.ConnectionId, ex); + reason = ConnectionEndReason.ConnectionReset; } } error = ex; @@ -468,20 +473,24 @@ public async Task ProcessRequestsAsync(IHttpApplication appl { 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 @@ public async Task ProcessRequestsAsync(IHttpApplication appl 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 @@ public async Task ProcessRequestsAsync(IHttpApplication appl } 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 @@ public async Task ProcessRequestAsync(IHttpApplication appli 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 @@ public async Task ProcessRequestAsync(IHttpApplication appli 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 @@ public async Task ProcessRequestAsync(IHttpApplication appli 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 64ce3f7cd3c5..ac8307de6e46 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs @@ -95,7 +95,8 @@ public void Dispose() } } - void IHttpOutputAborter.Abort(ConnectionAbortedException abortReason) + // In HTTP/1.x, this aborts the entire connection. For HTTP/3 we abort the stream. + 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 2fc0767afc3f..24afce638c3b 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs @@ -758,10 +758,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.UnexpectedFrame), // 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.UnexpectedFrame), _ => ProcessUnknownFrameAsync(), }; } @@ -779,7 +779,7 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication // 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) @@ -878,7 +878,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. @@ -886,7 +886,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..1de818af0600 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(); @@ -100,7 +104,7 @@ public async Task ProcessRequestsAsync(IHttpApplication http 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 +116,14 @@ public async Task ProcessRequestsAsync(IHttpApplication http { Log.LogCritical(0, ex, $"Unexpected exception in {nameof(HttpConnection)}.{nameof(ProcessRequestsAsync)}."); } + finally + { + // Before exiting HTTP layer, set the end reason on the context as a connection metrics tag. + if (_context.MetricsContext.ConnectionEndReason is { } connectionEndReason) + { + KestrelMetrics.AddConnectionEndReason(connectionMetricsTagsFeature, connectionEndReason); + } + } } private void AddMetricsHttpProtocolTag(string httpVersion) @@ -131,7 +143,7 @@ internal void Initialize(IRequestProcessor requestProcessor) _protocolSelectionState = ProtocolSelectionState.Selected; } - private void StopProcessingNextRequest() + private void StopProcessingNextRequest(ConnectionEndReason reason) { ProtocolSelectionState previousState; lock (_protocolSelectionLock) @@ -143,7 +155,7 @@ private void StopProcessingNextRequest() switch (previousState) { case ProtocolSelectionState.Selected: - _requestProcessor!.StopProcessingNextRequest(); + _requestProcessor!.StopProcessingNextRequest(reason); break; case ProtocolSelectionState.Aborted: break; @@ -169,7 +181,7 @@ private void OnConnectionClosed() } } - private void Abort(ConnectionAbortedException ex) + private void Abort(ConnectionAbortedException ex, ConnectionEndReason reason) { ProtocolSelectionState previousState; @@ -184,7 +196,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 +271,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 +281,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 @@ public HttpConnectionContext( 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 @@ public HttpMultiplexedConnectionContext( 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..d8c41361e567 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 ErrorTypeAttributeName = "error.type"; + public const string Http11 = "1.1"; public const string Http2 = "2"; public const string Http3 = "3"; @@ -79,7 +82,7 @@ public KestrelMetrics(IMeterFactory meterFactory) description: "Number of TLS handshakes that are currently in progress on the server."); } - public void ConnectionStart(in ConnectionMetricsContext metricsContext) + public void ConnectionStart(ConnectionMetricsContext metricsContext) { if (metricsContext.CurrentConnectionsCounterEnabled) { @@ -88,14 +91,14 @@ public void ConnectionStart(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void ConnectionStartCore(in ConnectionMetricsContext metricsContext) + private void ConnectionStartCore(ConnectionMetricsContext metricsContext) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); _activeConnectionsCounter.Add(1, tags); } - public void ConnectionStop(in ConnectionMetricsContext metricsContext, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) + public void ConnectionStop(ConnectionMetricsContext metricsContext, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) { if (metricsContext.CurrentConnectionsCounterEnabled || metricsContext.ConnectionDurationEnabled) { @@ -104,7 +107,7 @@ public void ConnectionStop(in ConnectionMetricsContext metricsContext, Exception } [MethodImpl(MethodImplOptions.NoInlining)] - private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) + private void ConnectionStopCore(ConnectionMetricsContext metricsContext, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); @@ -117,9 +120,14 @@ private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exce if (metricsContext.ConnectionDurationEnabled) { - if (exception != null) + // 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(ErrorTypeAttributeName, errorValue); + } + else if (exception != null) { - tags.Add("error.type", exception.GetType().FullName); + tags.TryAddTag(ErrorTypeAttributeName, exception.GetType().FullName); } // Add custom tags for duration. @@ -136,8 +144,10 @@ private void ConnectionStopCore(in ConnectionMetricsContext metricsContext, Exce } } - public void ConnectionRejected(in ConnectionMetricsContext metricsContext) + public void ConnectionRejected(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) { @@ -146,14 +156,14 @@ public void ConnectionRejected(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void ConnectionRejectedCore(in ConnectionMetricsContext metricsContext) + private void ConnectionRejectedCore(ConnectionMetricsContext metricsContext) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); _rejectedConnectionsCounter.Add(1, tags); } - public void ConnectionQueuedStart(in ConnectionMetricsContext metricsContext) + public void ConnectionQueuedStart(ConnectionMetricsContext metricsContext) { if (metricsContext.QueuedConnectionsCounterEnabled) { @@ -162,14 +172,14 @@ public void ConnectionQueuedStart(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void ConnectionQueuedStartCore(in ConnectionMetricsContext metricsContext) + private void ConnectionQueuedStartCore(ConnectionMetricsContext metricsContext) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); _queuedConnectionsCounter.Add(1, tags); } - public void ConnectionQueuedStop(in ConnectionMetricsContext metricsContext) + public void ConnectionQueuedStop(ConnectionMetricsContext metricsContext) { if (metricsContext.QueuedConnectionsCounterEnabled) { @@ -178,14 +188,14 @@ public void ConnectionQueuedStop(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void ConnectionQueuedStopCore(in ConnectionMetricsContext metricsContext) + private void ConnectionQueuedStopCore(ConnectionMetricsContext metricsContext) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); _queuedConnectionsCounter.Add(-1, tags); } - public void RequestQueuedStart(in ConnectionMetricsContext metricsContext, string httpVersion) + public void RequestQueuedStart(ConnectionMetricsContext metricsContext, string httpVersion) { if (metricsContext.QueuedRequestsCounterEnabled) { @@ -194,7 +204,7 @@ public void RequestQueuedStart(in ConnectionMetricsContext metricsContext, strin } [MethodImpl(MethodImplOptions.NoInlining)] - private void RequestQueuedStartCore(in ConnectionMetricsContext metricsContext, string httpVersion) + private void RequestQueuedStartCore(ConnectionMetricsContext metricsContext, string httpVersion) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); @@ -203,7 +213,7 @@ private void RequestQueuedStartCore(in ConnectionMetricsContext metricsContext, _queuedRequestsCounter.Add(1, tags); } - public void RequestQueuedStop(in ConnectionMetricsContext metricsContext, string httpVersion) + public void RequestQueuedStop(ConnectionMetricsContext metricsContext, string httpVersion) { if (metricsContext.QueuedRequestsCounterEnabled) { @@ -212,7 +222,7 @@ public void RequestQueuedStop(in ConnectionMetricsContext metricsContext, string } [MethodImpl(MethodImplOptions.NoInlining)] - private void RequestQueuedStopCore(in ConnectionMetricsContext metricsContext, string httpVersion) + private void RequestQueuedStopCore(ConnectionMetricsContext metricsContext, string httpVersion) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); @@ -221,7 +231,7 @@ private void RequestQueuedStopCore(in ConnectionMetricsContext metricsContext, s _queuedRequestsCounter.Add(-1, tags); } - public void RequestUpgradedStart(in ConnectionMetricsContext metricsContext) + public void RequestUpgradedStart(ConnectionMetricsContext metricsContext) { if (metricsContext.CurrentUpgradedRequestsCounterEnabled) { @@ -230,14 +240,14 @@ public void RequestUpgradedStart(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void RequestUpgradedStartCore(in ConnectionMetricsContext metricsContext) + private void RequestUpgradedStartCore(ConnectionMetricsContext metricsContext) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); _currentUpgradedRequestsCounter.Add(1, tags); } - public void RequestUpgradedStop(in ConnectionMetricsContext metricsContext) + public void RequestUpgradedStop(ConnectionMetricsContext metricsContext) { if (metricsContext.CurrentUpgradedRequestsCounterEnabled) { @@ -246,14 +256,14 @@ public void RequestUpgradedStop(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void RequestUpgradedStopCore(in ConnectionMetricsContext metricsContext) + private void RequestUpgradedStopCore(ConnectionMetricsContext metricsContext) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); _currentUpgradedRequestsCounter.Add(-1, tags); } - public void TlsHandshakeStart(in ConnectionMetricsContext metricsContext) + public void TlsHandshakeStart(ConnectionMetricsContext metricsContext) { if (metricsContext.CurrentTlsHandshakesCounterEnabled) { @@ -262,7 +272,7 @@ public void TlsHandshakeStart(in ConnectionMetricsContext metricsContext) } [MethodImpl(MethodImplOptions.NoInlining)] - private void TlsHandshakeStartCore(in ConnectionMetricsContext metricsContext) + private void TlsHandshakeStartCore(ConnectionMetricsContext metricsContext) { // Tags must match TLS handshake end. var tags = new TagList(); @@ -270,7 +280,7 @@ private void TlsHandshakeStartCore(in ConnectionMetricsContext metricsContext) _activeTlsHandshakesCounter.Add(1, tags); } - public void TlsHandshakeStop(in ConnectionMetricsContext metricsContext, long startTimestamp, long currentTimestamp, SslProtocols? protocol = null, Exception? exception = null) + public void TlsHandshakeStop(ConnectionMetricsContext metricsContext, long startTimestamp, long currentTimestamp, SslProtocols? protocol = null, Exception? exception = null) { if (metricsContext.CurrentTlsHandshakesCounterEnabled || _tlsHandshakeDuration.Enabled) { @@ -279,7 +289,7 @@ public void TlsHandshakeStop(in ConnectionMetricsContext metricsContext, long st } [MethodImpl(MethodImplOptions.NoInlining)] - private void TlsHandshakeStopCore(in ConnectionMetricsContext metricsContext, long startTimestamp, long currentTimestamp, SslProtocols? protocol = null, Exception? exception = null) + private void TlsHandshakeStopCore(ConnectionMetricsContext metricsContext, long startTimestamp, long currentTimestamp, SslProtocols? protocol = null, Exception? exception = null) { var tags = new TagList(); InitializeConnectionTags(ref tags, metricsContext); @@ -301,7 +311,8 @@ private void TlsHandshakeStopCore(in ConnectionMetricsContext metricsContext, lo } if (exception != null) { - tags.Add("error.type", exception.GetType().FullName); + // Set exception name as error.type if there isn't already a value. + tags.TryAddTag(ErrorTypeAttributeName, exception.GetType().FullName); } var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); @@ -352,9 +363,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 +416,110 @@ 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(ErrorTypeAttributeName, errorTypeValue); + } + } + } + + public static void AddConnectionEndReason(ConnectionMetricsContext? context, ConnectionEndReason reason, bool overwrite = false) + { + Debug.Assert(reason != ConnectionEndReason.Unset); + + if (context != null) + { + // Set end reason when either: + // - Overwrite is true. For example, AppShutdownTimeout reason is forced when shutting down + // the app reguardless of whether there is already a value. + // - New reason is an error type and there isn't already an error type set. + // In other words, first error wins. + if (overwrite) + { + Debug.Assert(TryGetErrorType(reason, out _), "Overwrite should only be set for an error reason."); + context.ConnectionEndReason = reason; + } + else if (TryGetErrorType(reason, out _)) + { + if (context.ConnectionEndReason == null) + { + 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.RequestNoKeepAlive => null, // Not an error + ConnectionEndReason.ResponseNoKeepAlive => null, // Not an error + ConnectionEndReason.ErrorAfterStartingResponse => "error_after_starting_response", + 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.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.InvalidWindowUpdateSize => "invalid_window_update_size", + 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.InvalidBodyReaderState => "invalid_body_reader_state", + ConnectionEndReason.ServerTimeout => "server_timeout", + ConnectionEndReason.StreamCreationError => "stream_creation_error", + ConnectionEndReason.IOError => "io_error", + ConnectionEndReason.AppShutdownTimeout => "app_shutdown_timeout", + ConnectionEndReason.TlsHandshakeFailed => "tls_handshake_failed", + ConnectionEndReason.InvalidRequestLine => "invalid_request_line", + ConnectionEndReason.TlsNotSupported => "tls_not_supported", + ConnectionEndReason.MaxRequestBodySizeExceeded => "max_request_body_size_exceeded", + ConnectionEndReason.UnexpectedEndOfRequestContent => "unexpected_end_of_request_content", + ConnectionEndReason.MaxConcurrentConnectionsExceeded => "max_concurrent_connections_exceeded", + ConnectionEndReason.MaxRequestHeadersTotalSizeExceeded => "max_request_headers_total_size_exceeded", + ConnectionEndReason.MaxRequestHeaderCountExceeded => "max_request_header_count_exceeded", + ConnectionEndReason.ResponseContentLengthMismatch => "response_content_length_mismatch", + _ => 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..2ddbe870a71c 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.Error, @"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.AppShutdownTimeout, 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 @@ public async Task OnConnectionAsync(ConnectionContext context) 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..627c912c5a99 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.AppShutdownTimeout); 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.AppShutdownTimeout); _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.AppShutdownTimeout); 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 c4008d7b2208..3fd5631ed42c 100644 --- a/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs +++ b/src/Servers/Kestrel/Core/test/Http1/Http1ConnectionTestsBase.cs @@ -38,8 +38,12 @@ protected override void Initialize(TestContext context, MethodInfo methodInfo, o _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 @@ protected override void Initialize(TestContext context, MethodInfo methodInfo, o 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 @@ private TestHttpOutputProducer CreateOutputProducer( 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..38779ac9b77d 100644 --- a/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpResponseHeadersTests.cs @@ -1,21 +1,18 @@ // 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.Pipelines; using System.Text; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Primitives; using Moq; -using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests; @@ -28,12 +25,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..6de3f9771c3a 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 Dictionary 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/MetricsAssert.cs b/src/Servers/Kestrel/shared/test/MetricsAssert.cs new file mode 100644 index 000000000000..c916b06f3217 --- /dev/null +++ b/src/Servers/Kestrel/shared/test/MetricsAssert.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; + +namespace Microsoft.AspNetCore.InternalTesting; + +internal static class MetricsAssert +{ + public static void Equal(ConnectionEndReason expectedReason, string errorType) + { + Assert.Equal(KestrelMetrics.GetErrorType(expectedReason), errorType); + } + + public static void Equal(ConnectionEndReason expectedReason, IReadOnlyDictionary tags) + { + Equal(expectedReason, (string) tags[KestrelMetrics.ErrorTypeAttributeName]); + } + + public static void NoError(IReadOnlyDictionary tags) + { + if (tags.TryGetValue(KestrelMetrics.ErrorTypeAttributeName, out var error)) + { + Assert.Fail($"Tag collection contains {KestrelMetrics.ErrorTypeAttributeName} with value {error}."); + } + } +} 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 @@ public static HttpConnectionContext CreateHttpConnectionContext( 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 @@ public static HttpConnectionContext CreateHttpConnectionContext( memoryPool ?? MemoryPool.Shared, localEndPoint, remoteEndPoint, - CreateMetricsContext(connectionContext)); + metricsContext ?? CreateMetricsContext(connectionContext)); context.TimeoutControl = timeoutControl; context.Transport = transport; @@ -79,18 +80,23 @@ public static HttpMultiplexedConnectionContext CreateHttp3ConnectionContext( 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 @@ public static Http3StreamContext CreateHttp3StreamContext( }; } - 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..8ae6842a2e62 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,8 @@ public async Task RequestAbortedTokenFiredOnClientFIN() await host.StopAsync(); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Fact] @@ -669,6 +677,9 @@ await connection1.Send( [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 +699,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 +723,8 @@ public async Task AbortingTheConnection(bool fin) await host.StopAsync(); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.AbortedByApp, m.Tags)); } [Theory] diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index eb01e1eebe09..95dd1549615f 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,8 @@ await connection.Send( // RequestAborted tripped await requestAbortedWh.Task.DefaultTimeout(); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Theory] @@ -427,7 +427,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 +456,11 @@ await connection.Send( 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(); + MetricsAssert.NoError(measurement.Tags); } [Theory] @@ -492,7 +500,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 +595,8 @@ await connection.Send( logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MinResponseDataRate, m.Tags)); } [Theory] @@ -742,7 +755,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 +843,8 @@ await connection.Send( await AssertStreamAborted(connection.Stream, responseSize); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MinResponseDataRate, m.Tags)); } [ConditionalFact] @@ -1000,7 +1018,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 +1084,8 @@ await connection.Receive( 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 => MetricsAssert.NoError(m.Tags)); } 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..b325a4e4155a 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; @@ -27,7 +28,8 @@ public Task TestInvalidRequestLines(string request, string expectedExceptionMess return TestBadRequest( request, "400 Bad Request", - expectedExceptionMessage); + expectedExceptionMessage, + ConnectionEndReason.InvalidRequestLine); } [Theory] @@ -37,7 +39,8 @@ public Task TestInvalidRequestLinesWithUnrecognizedVersion(string httpVersion) return TestBadRequest( $"GET / {httpVersion}\r\n", "505 HTTP Version Not Supported", - CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(httpVersion)); + CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(httpVersion), + ConnectionEndReason.InvalidHttpVersion); } [Theory] @@ -47,7 +50,8 @@ public Task TestInvalidHeaders(string rawHeaders, string expectedExceptionMessag return TestBadRequest( $"GET / HTTP/1.1\r\n{rawHeaders}", "400 Bad Request", - expectedExceptionMessage); + expectedExceptionMessage, + ConnectionEndReason.InvalidRequestHeaders); } public static Dictionary BadHeaderData => new Dictionary @@ -77,7 +81,8 @@ public Task BadRequestWhenHeaderNameContainsNonASCIIOrNullCharacters(string data return TestBadRequest( $"GET / HTTP/1.1\r\n{header}\r\n\r\n", "400 Bad Request", - errorMessage); + errorMessage, + ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -88,7 +93,8 @@ public Task BadRequestIfMethodRequiresLengthButNoContentLengthInHttp10Request(st return TestBadRequest( $"{method} / HTTP/1.0\r\n\r\n", "400 Bad Request", - CoreStrings.FormatBadRequest_LengthRequiredHttp10(method)); + CoreStrings.FormatBadRequest_LengthRequiredHttp10(method), + ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -99,7 +105,8 @@ public Task BadRequestIfContentLengthInvalid(string contentLength) return TestBadRequest( $"POST / HTTP/1.1\r\nHost:\r\nContent-Length: {contentLength}\r\n\r\n", "400 Bad Request", - CoreStrings.FormatBadRequest_InvalidContentLength_Detail(contentLength)); + CoreStrings.FormatBadRequest_InvalidContentLength_Detail(contentLength), + ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -111,6 +118,7 @@ public Task RejectsIncorrectMethods(string request, string allowedMethod) $"{request} HTTP/1.1\r\n", "405 Method Not Allowed", CoreStrings.BadRequest_MethodNotAllowed, + ConnectionEndReason.InvalidRequestHeaders, $"Allow: {allowedMethod}"); } @@ -120,7 +128,8 @@ public Task BadRequestIfHostHeaderMissing() return TestBadRequest( "GET / HTTP/1.1\r\n\r\n", "400 Bad Request", - CoreStrings.BadRequest_MissingHostHeader); + CoreStrings.BadRequest_MissingHostHeader, + ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -128,7 +137,8 @@ public Task BadRequestIfMultipleHostHeaders() { return TestBadRequest("GET / HTTP/1.1\r\nHost: localhost\r\nHost: localhost\r\n\r\n", "400 Bad Request", - CoreStrings.BadRequest_MultipleHostHeaders); + CoreStrings.BadRequest_MultipleHostHeaders, + ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -138,7 +148,8 @@ public Task BadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestTarget return TestBadRequest( $"{requestTarget} HTTP/1.1\r\nHost: {host}\r\n\r\n", "400 Bad Request", - CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim())); + CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(host.Trim()), + ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -146,25 +157,33 @@ public Task BadRequestIfHostHeaderDoesNotMatchRequestTarget(string requestTarget [InlineData("Host: www.notfoo.com")] // Syntactically correct but not matching public async Task CanOptOutOfBadRequestIfHostHeaderDoesNotMatchRequestTarget(string hostHeader) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var receivedHost = StringValues.Empty; - await using var server = new TestServer(context => + await using (var server = new TestServer(context => { receivedHost = context.Request.Headers.Host; return Task.CompletedTask; - }, new TestServiceContext(LoggerFactory) + }, new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { ServerOptions = new KestrelServerOptions() { AllowHostHeaderOverride = true, } - }); - using var client = server.CreateConnection(); - - await client.SendAll($"GET http://www.foo.com/api/data HTTP/1.1\r\n{hostHeader}\r\n\r\n"); + })) + { + using (var client = server.CreateConnection()) + { + await client.SendAll($"GET http://www.foo.com/api/data HTTP/1.1\r\n{hostHeader}\r\n\r\n"); - await client.Receive("HTTP/1.1 200 OK"); + await client.Receive("HTTP/1.1 200 OK"); + } + } Assert.Equal("www.foo.com:80", receivedHost); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Fact] @@ -173,7 +192,8 @@ public Task BadRequestFor10BadHostHeaderFormat() return TestBadRequest( $"GET / HTTP/1.0\r\nHost: a=b\r\n\r\n", "400 Bad Request", - CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b")); + CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), + ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -182,7 +202,8 @@ public Task BadRequestFor11BadHostHeaderFormat() return TestBadRequest( $"GET / HTTP/1.1\r\nHost: a=b\r\n\r\n", "400 Bad Request", - CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b")); + CoreStrings.FormatBadRequest_InvalidHostHeader_Detail("a=b"), + ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -210,7 +231,10 @@ await connection.SendAll( [Fact] public async Task TestRequestSplitting() { - 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()) { @@ -221,12 +245,18 @@ await client.SendAll( await client.Receive("HTTP/1.1 400"); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), + m => MetricsAssert.Equal(ConnectionEndReason.InvalidRequestLine, m.Tags)); } [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 +268,9 @@ public async Task BadRequestForHttp2() Assert.Empty(await client.Stream.ReadUntilEndAsync().DefaultTimeout()); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), + m => MetricsAssert.Equal(ConnectionEndReason.InvalidHttpVersion, m.Tags)); } [Fact] @@ -246,7 +279,8 @@ public Task BadRequestForAbsoluteFormTargetWithNonAsciiChars() return TestBadRequest( $"GET http://localhost/ÿÿÿ HTTP/1.1\r\n", "400 Bad Request", - CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail("http://localhost/\\xFF\\xFF\\xFF")); + CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail("http://localhost/\\xFF\\xFF\\xFF"), + ConnectionEndReason.InvalidRequestLine); } private class BadRequestEventListener : IObserver>, IDisposable @@ -276,7 +310,7 @@ public void OnCompleted() { } public virtual void Dispose() => _subscription.Dispose(); } - private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, string expectedAllowHeader = null) + private async Task TestBadRequest(string request, string expectedResponseStatusCode, string expectedExceptionMessage, ConnectionEndReason reason, string expectedAllowHeader = null) { BadHttpRequestException loggedException = null; @@ -303,7 +337,10 @@ private async Task TestBadRequest(string request, string expectedResponseStatusC } }); - await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory) { DiagnosticSource = diagListener })) + 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)) { DiagnosticSource = diagListener })) { using (var connection = server.CreateConnection()) { @@ -319,6 +356,8 @@ private async Task TestBadRequest(string request, string expectedResponseStatusC Assert.True(badRequestEventListener.EventFired); Assert.Equal("Microsoft.AspNetCore.Server.Kestrel.BadRequest", eventProviderName); Assert.Contains(expectedExceptionMessage, exceptionString); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(reason, m.Tags)); } [Theory] @@ -345,7 +384,10 @@ public async Task ExtraLinesBetweenRequestsIgnored(string extraLines) var diagListener = new DiagnosticListener("NotBadRequestTestsDiagListener"); var badRequestEventListener = new BadRequestEventListener(diagListener, (pair) => { }); - await using (var server = new TestServer(context => context.Request.Body.DrainAsync(default), new TestServiceContext(LoggerFactory) { DiagnosticSource = diagListener })) + 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.Request.Body.DrainAsync(default), new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { DiagnosticSource = diagListener })) { using (var connection = server.CreateConnection()) { @@ -387,6 +429,8 @@ await connection.Receive( Assert.Null(loggedException); // Verify DiagnosticSource event for bad request Assert.False(badRequestEventListener.EventFired); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Fact] @@ -406,7 +450,10 @@ public async Task ExtraLinesIgnoredBetweenAdjacentRequests() var diagListener = new DiagnosticListener("NotBadRequestTestsDiagListener"); var badRequestEventListener = new BadRequestEventListener(diagListener, (pair) => { }); - await using (var server = new TestServer(context => context.Request.Body.DrainAsync(default), new TestServiceContext(LoggerFactory) { DiagnosticSource = diagListener })) + 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.Request.Body.DrainAsync(default), new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { DiagnosticSource = diagListener })) { using (var connection = server.CreateConnection()) { @@ -451,6 +498,8 @@ await connection.Receive( Assert.Null(loggedException); // Verify DiagnosticSource event for bad request Assert.False(badRequestEventListener.EventFired); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Theory] @@ -476,7 +525,10 @@ public async Task ExtraLinesAtEndOfConnectionIgnored(string extraLines) var diagListener = new DiagnosticListener("NotBadRequestTestsDiagListener"); var badRequestEventListener = new BadRequestEventListener(diagListener, (pair) => { }); - await using (var server = new TestServer(context => context.Request.Body.DrainAsync(default), new TestServiceContext(LoggerFactory) { DiagnosticSource = diagListener })) + 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.Request.Body.DrainAsync(default), new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)) { DiagnosticSource = diagListener })) { using (var connection = server.CreateConnection()) { @@ -504,6 +556,8 @@ await connection.Receive( Assert.Null(loggedException); // Verify DiagnosticSource event for bad request Assert.False(badRequestEventListener.EventFired); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } private async Task ReceiveBadRequestResponse(InMemoryConnection connection, string expectedResponseStatusCode, string expectedDateHeaderValue, string expectedAllowHeader = null) diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 1642d31d7a82..f5ee2b28228e 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 @@ await connection.ReceiveEnd( [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,8 @@ await connection.SendAll( Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, badReqEx.Reason); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.UnexpectedEndOfRequestContent, m.Tags)); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ConnectionLimitTests.cs index 859ecad6373a..bc259f9f80b5 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.ErrorTypeAttributeName, 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..99873c279ba6 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 @@ public async Task ImmediateFinAfterOnConnectionAsyncClosesGracefully(RequestDele [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 @@ public async Task ImmediateFinAfterThrowingClosesGracefully(RequestDelegate requ await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => + { + Assert.Equal(typeof(InvalidOperationException).FullName, m.Tags[KestrelMetrics.ErrorTypeAttributeName]); + }); } [Theory] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs index d0420bcb71c1..be16c4d5e6bd 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 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -157,6 +160,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -206,6 +210,7 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.Same(path1, path2); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -273,6 +278,7 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.Equal(StringValues.Empty, contentTypeValue2); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } private class ResponseTrailersWrapper : IHeaderDictionary @@ -390,6 +396,7 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.NotSame(trailersFirst, trailersLast); await StopConnectionAsync(expectedLastStreamId: 5, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -423,6 +430,7 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -547,6 +555,7 @@ await ExpectAsync(Http2FrameType.HEADERS, 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 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.True(((Http2OutputProducer)pooledStream.Output)._disposed); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -701,6 +713,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorFrameOverLimit(length, Http2PeerSettings.MinAllowedMaxFrameSize)); + AssertConnectionEndReason(ConnectionEndReason.MaxFrameLengthExceeded); } [Fact] @@ -733,6 +746,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -757,6 +771,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -784,6 +799,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -878,6 +894,7 @@ await ExpectAsync(Http2FrameType.DATA, 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 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -1010,6 +1029,7 @@ await ExpectAsync(Http2FrameType.DATA, 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 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); foreach (var frame in dataFrames) { @@ -1200,6 +1221,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Theory] @@ -1227,6 +1249,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_helloWorldBytes.AsSpan().SequenceEqual(dataFrame.PayloadSequence.ToArray())); } @@ -1296,6 +1319,7 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); Assert.True(_maxData.AsSpan().SequenceEqual(dataFrame3.PayloadSequence.ToArray())); @@ -1336,6 +1360,7 @@ await ExpectAsync(Http2FrameType.HEADERS, 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 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); foreach (var frame in dataFrames) { @@ -1451,6 +1477,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.DATA)); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -1465,6 +1492,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.DATA, streamId: 2)); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -1480,6 +1508,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.DATA)); + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -1495,6 +1524,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.DATA)); + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -1510,6 +1540,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.DATA, expectedLength: 1)); + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -1525,6 +1556,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.DATA, streamId: 1, headersStreamId: 1)); + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -1539,6 +1571,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdle(Http2FrameType.DATA, streamId: 1)); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -1556,6 +1589,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1)); + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); } [Fact] @@ -1581,6 +1615,7 @@ await WaitForConnectionErrorAsync( CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); } [Fact] @@ -1615,6 +1650,7 @@ await ExpectAsync(Http2FrameType.HEADERS, 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 @@ await WaitForStreamErrorAsync( 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 @@ await WaitForStreamErrorAsync( tcs.SetResult(); await StopConnectionAsync(streamId, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] @@ -1786,6 +1829,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); } [Fact] @@ -1808,6 +1853,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorFlowControlWindowExceeded); + + AssertConnectionEndReason(ConnectionEndReason.FlowControlWindowExceeded); } [Fact] @@ -1837,6 +1884,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorFlowControlWindowExceeded); + + AssertConnectionEndReason(ConnectionEndReason.FlowControlWindowExceeded); } [Fact] @@ -1918,6 +1967,8 @@ await ExpectAsync(Http2FrameType.DATA, await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); await WaitForAllStreamsAsync(); + + AssertConnectionNoError(); } [Fact] @@ -2002,6 +2053,8 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2054,6 +2107,8 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2071,6 +2126,8 @@ await ExpectAsync(Http2FrameType.HEADERS, VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2091,6 +2148,8 @@ await ExpectAsync(Http2FrameType.HEADERS, VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2108,6 +2167,8 @@ await ExpectAsync(Http2FrameType.HEADERS, VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2128,6 +2189,8 @@ await ExpectAsync(Http2FrameType.HEADERS, VerifyDecodedRequestHeaders(_browserRequestHeaders); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -2174,6 +2237,8 @@ await ExpectAsync(Http2FrameType.HEADERS, } await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2206,6 +2271,8 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2255,6 +2322,8 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2289,6 +2358,8 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 3); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2447,6 +2518,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.HEADERS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2461,6 +2534,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.HEADERS, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2487,6 +2562,8 @@ await WaitForConnectionErrorAsync( CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1) }); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2504,6 +2581,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.FrameAfterStreamClose); } [Fact] @@ -2526,6 +2605,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 3, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -2543,6 +2624,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.HEADERS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -2557,6 +2640,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.HEADERS, expectedLength: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Theory] @@ -2573,6 +2658,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorPaddingTooLong(Http2FrameType.HEADERS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidDataPadding); } [Fact] @@ -2588,6 +2675,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.HEADERS, streamId: 3, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -2602,6 +2691,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamSelfDependency(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.StreamSelfDependency); } [Fact] @@ -2616,6 +2707,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, expectedErrorMessage: SR.net_http_hpack_incomplete_header_block); + + AssertConnectionEndReason(ConnectionEndReason.ErrorReadingHeaders); } [Fact] @@ -2644,6 +2737,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, expectedErrorMessage: SR.net_http_hpack_bad_integer); + + AssertConnectionEndReason(ConnectionEndReason.ErrorReadingHeaders); } [Theory] @@ -2660,6 +2755,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: expectedErrorMessage); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -2678,6 +2775,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorHeadersWithTrailersNoEndStream); + + AssertConnectionEndReason(ConnectionEndReason.MissingStreamEnd); } [Theory] @@ -2692,6 +2791,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.HttpErrorHeaderNameUppercase); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2705,7 +2806,10 @@ public Task HEADERS_Received_HeaderBlockContainsUnknownPseudoHeaderField_Connect new KeyValuePair(":unknown", "0"), }; - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorUnknownPseudoHeaderField); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + expectedErrorMessage: CoreStrings.HttpErrorUnknownPseudoHeaderField, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2719,14 +2823,20 @@ public Task HEADERS_Received_HeaderBlockContainsResponsePseudoHeaderField_Connec new KeyValuePair(InternalHeaderNames.Status, "200"), }; - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorResponsePseudoHeaderField); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + expectedErrorMessage: CoreStrings.HttpErrorResponsePseudoHeaderField, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } [Theory] [MemberData(nameof(DuplicatePseudoHeaderFieldData))] public Task HEADERS_Received_HeaderBlockContainsDuplicatePseudoHeaderField_ConnectionError(IEnumerable> headers) { - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorDuplicatePseudoHeaderField); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + expectedErrorMessage: CoreStrings.HttpErrorDuplicatePseudoHeaderField, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -2743,16 +2853,21 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] [MemberData(nameof(PseudoHeaderFieldAfterRegularHeadersData))] public Task HEADERS_Received_HeaderBlockContainsPseudoHeaderFieldAfterRegularHeaders_ConnectionError(IEnumerable> headers) { - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + expectedErrorMessage: CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } - private async Task HEADERS_Received_InvalidHeaderFields_ConnectionError(IEnumerable> headers, string expectedErrorMessage) + private async Task HEADERS_Received_InvalidHeaderFields_ConnectionError(IEnumerable> headers, string expectedErrorMessage, ConnectionEndReason expectedEndReason) { await InitializeConnectionAsync(_noopApplication); await StartStreamAsync(1, headers, endStream: true); @@ -2761,6 +2876,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: expectedErrorMessage); + + AssertConnectionEndReason(expectedEndReason); } [Theory] @@ -2782,6 +2899,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2811,7 +2930,10 @@ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError() new KeyValuePair("p", _4kHeaderValue), }; - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + CoreStrings.BadRequest_HeadersExceedMaxTotalSize, + expectedEndReason: ConnectionEndReason.MaxRequestHeadersTotalSizeExceeded); } [Fact] @@ -2830,7 +2952,10 @@ public Task HEADERS_Received_TooManyHeaders_ConnectionError() headers.Add(new KeyValuePair(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture))); } - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_TooManyHeaders); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + CoreStrings.BadRequest_TooManyHeaders, + expectedEndReason: ConnectionEndReason.MaxRequestHeaderCountExceeded); } [Fact] @@ -2844,7 +2969,10 @@ public Task HEADERS_Received_InvalidCharacters_ConnectionError() new KeyValuePair("Custom", "val\0ue"), }; - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.BadRequest_MalformedRequestInvalidHeaders); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + CoreStrings.BadRequest_MalformedRequestInvalidHeaders, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2858,7 +2986,10 @@ public Task HEADERS_Received_HeaderBlockContainsConnectionHeader_ConnectionError new KeyValuePair("connection", "keep-alive") }; - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.HttpErrorConnectionSpecificHeaderField); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + CoreStrings.HttpErrorConnectionSpecificHeaderField, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2872,7 +3003,10 @@ public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsNotTrailers_Conn new KeyValuePair("te", "trailers, deflate") }; - return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.HttpErrorConnectionSpecificHeaderField); + return HEADERS_Received_InvalidHeaderFields_ConnectionError( + headers, + CoreStrings.HttpErrorConnectionSpecificHeaderField, + expectedEndReason: ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -2896,6 +3030,8 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -2914,6 +3050,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 +3093,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.PRIORITY)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -2969,6 +3109,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.PRIORITY, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -2985,6 +3127,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.PRIORITY, expectedLength: 5)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3000,6 +3144,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.PRIORITY, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -3014,6 +3160,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamSelfDependency(Http2FrameType.PRIORITY, 1)); + + AssertConnectionEndReason(ConnectionEndReason.StreamSelfDependency); } [Fact] @@ -3134,6 +3282,8 @@ await ExpectAsync(Http2FrameType.DATA, await WaitForAllStreamsAsync(); Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -3218,6 +3368,8 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -3266,6 +3418,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdZero(Http2FrameType.RST_STREAM)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -3280,6 +3434,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.RST_STREAM, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -3294,6 +3450,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdle(Http2FrameType.RST_STREAM, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -3313,6 +3471,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.RST_STREAM, expectedLength: 4)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3328,6 +3488,8 @@ await WaitForConnectionErrorAsync( 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 +3515,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 +3541,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 +3565,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 +3641,8 @@ await ExpectAsync(Http2FrameType.SETTINGS, withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3532,6 +3702,8 @@ await ExpectAsync(Http2FrameType.SETTINGS, withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3567,6 +3739,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdNotZero(Http2FrameType.SETTINGS)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -3593,6 +3767,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: expectedErrorCode, expectedErrorMessage: CoreStrings.FormatHttp2ErrorSettingsParameterOutOfRange(parameter)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidSettings); } [Fact] @@ -3608,6 +3784,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.SETTINGS, streamId: 0, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Theory] @@ -3624,6 +3802,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorSettingsAckLengthNotZero); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Theory] @@ -3643,6 +3823,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorSettingsLengthNotMultipleOfSix); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3667,6 +3849,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorInitialWindowSizeInvalid); + + AssertConnectionEndReason(ConnectionEndReason.InvalidSettings); } [Fact] @@ -3724,6 +3908,8 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3763,6 +3949,8 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3844,6 +4032,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorPushPromiseReceived); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -3883,6 +4073,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.PING, streamId: 0, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -3897,6 +4089,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdNotZero(Http2FrameType.PING)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -3915,6 +4109,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.PING, expectedLength: 8)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -3925,6 +4121,8 @@ public async Task GOAWAY_Received_ConnectionStops() await SendGoAwayAsync(); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3938,6 +4136,8 @@ public async Task GOAWAY_Received_ConnectionLifetimeNotification_Cancelled() Assert.True(lifetime.ConnectionClosedRequested.IsCancellationRequested); await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -3983,6 +4183,8 @@ await ExpectAsync(Http2FrameType.DATA, TriggerTick(); await WaitForConnectionStopAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); await _closedStateReached.Task.DefaultTimeout(); + + AssertConnectionNoError(); } [Fact] @@ -4086,6 +4288,8 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -4163,6 +4367,8 @@ await ExpectAsync(Http2FrameType.HEADERS, Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionNoError(); } [Fact] @@ -4177,6 +4383,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdNotZero(Http2FrameType.GOAWAY)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4192,6 +4400,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.GOAWAY, streamId: 0, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -4206,6 +4416,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdEven(Http2FrameType.WINDOW_UPDATE, streamId: 2)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4221,6 +4433,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.WINDOW_UPDATE, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Theory] @@ -4239,6 +4453,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FRAME_SIZE_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorUnexpectedFrameLength(Http2FrameType.WINDOW_UPDATE, expectedLength: 4)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidFrameLength); } [Fact] @@ -4253,6 +4469,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateIncrementZero); + + AssertConnectionEndReason(ConnectionEndReason.InvalidWindowUpdateSize); } [Fact] @@ -4268,6 +4486,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateIncrementZero); + + AssertConnectionEndReason(ConnectionEndReason.InvalidWindowUpdateSize); } [Fact] @@ -4282,6 +4502,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamIdle(Http2FrameType.WINDOW_UPDATE, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4299,6 +4521,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.FLOW_CONTROL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateSizeInvalid); + + AssertConnectionEndReason(ConnectionEndReason.InvalidWindowUpdateSize); } [Fact] @@ -4318,6 +4542,8 @@ await WaitForStreamErrorAsync( expectedErrorMessage: CoreStrings.Http2ErrorWindowUpdateSizeInvalid); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4409,6 +4635,8 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4596,6 +4824,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(Http2FrameType.CONTINUATION, streamId: 3, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -4611,6 +4841,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.COMPRESSION_ERROR, expectedErrorMessage: SR.net_http_hpack_incomplete_header_block); + + AssertConnectionEndReason(ConnectionEndReason.ErrorReadingHeaders); } [Theory] @@ -4628,6 +4860,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: expectedErrorMessage); + + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Theory] @@ -4651,6 +4885,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.STREAM_CLOSED, expectedErrorMessage: CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Theory] @@ -4668,6 +4904,8 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4708,6 +4946,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 +4965,8 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0); await StopConnectionAsync(0, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -4740,6 +4982,8 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.FormatHttp2ErrorHeadersInterleaved(frameType: 42, streamId: 1, headersStreamId: 1)); + + AssertConnectionEndReason(ConnectionEndReason.UnexpectedFrame); } [Fact] @@ -4765,6 +5009,8 @@ await WaitForConnectionErrorAsync( Assert.Contains(1, _abortedStreamIds); Assert.Contains(3, _abortedStreamIds); Assert.Contains(5, _abortedStreamIds); + + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); } [Fact] @@ -4778,6 +5024,8 @@ public async Task ConnectionResetLoggedWithActiveStreams() await StopConnectionAsync(1, ignoreNonGoAwayFrames: false); Assert.Single(LogMessages, m => m.Exception is ConnectionResetException); + + AssertConnectionEndReason(ConnectionEndReason.ConnectionReset); } [Fact] @@ -4789,6 +5037,8 @@ public async Task ConnectionResetNotLoggedWithNoActiveStreams() await WaitForConnectionStopAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); Assert.DoesNotContain(LogMessages, m => m.Exception is ConnectionResetException); + + AssertConnectionNoError(); } [Fact] @@ -4802,6 +5052,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 +5061,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 +5079,8 @@ public async Task CompletionSendsFinalGOAWAY() await _closedStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), 0, Http2ErrorCode.NO_ERROR); + + AssertConnectionNoError(); } [Fact] @@ -4869,6 +5125,8 @@ await ExpectAsync(Http2FrameType.HEADERS, _pair.Application.Input.CancelPendingRead(); result = await readTask; Assert.True(result.IsCompleted); + + AssertConnectionNoError(); } [Fact] @@ -4878,7 +5136,7 @@ public async Task StopProcessingNextRequestSendsGracefulGOAWAYThenFinalGOAWAYWhe await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - _connection.StopProcessingNextRequest(); + _connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdownTimeout); await _closingStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), Int32.MaxValue, Http2ErrorCode.NO_ERROR); @@ -4900,6 +5158,8 @@ await ExpectAsync(Http2FrameType.DATA, TriggerTick(); await _closedStateReached.Task.DefaultTimeout(); VerifyGoAway(await ReceiveFrameAsync(), 1, Http2ErrorCode.NO_ERROR); + + AssertConnectionEndReason(ConnectionEndReason.AppShutdownTimeout); } [Fact] @@ -4910,7 +5170,7 @@ public async Task AcceptNewStreamsDuringClosingConnection() await StartStreamAsync(1, _browserRequestHeaders, endStream: false); - _connection.StopProcessingNextRequest(); + _connection.StopProcessingNextRequest(ConnectionEndReason.AppShutdownTimeout); VerifyGoAway(await ReceiveFrameAsync(), Int32.MaxValue, Http2ErrorCode.NO_ERROR); await _closingStateReached.Task.DefaultTimeout(); @@ -4947,6 +5207,8 @@ await ExpectAsync(Http2FrameType.DATA, TriggerTick(); await WaitForConnectionStopAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false); + + AssertConnectionEndReason(ConnectionEndReason.AppShutdownTimeout); } [Fact] @@ -4967,6 +5229,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 +5249,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 +5268,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 +5321,8 @@ await ExpectAsync(Http2FrameType.HEADERS, } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -5097,6 +5367,8 @@ public async Task AbortedStream_ResetsAndDrainsRequest(int intFinalFrameType) } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -5141,6 +5413,8 @@ public async Task ResetStream_ResetsAndDrainsRequest(int intFinalFrameType) } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Theory] @@ -5225,6 +5499,8 @@ await ExpectAsync(Http2FrameType.SETTINGS, } await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + AssertConnectionNoError(); } [Fact] @@ -5295,6 +5571,8 @@ await ExpectAsync(Http2FrameType.SETTINGS, 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 +5617,7 @@ await InitializeConnectionAsync(async context => Assert.Equal(streamPayload, streamResponse); await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: true); + AssertConnectionNoError(); } [Theory] @@ -5377,6 +5656,7 @@ await WaitForConnectionErrorAsync( CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.DATA, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.DATA, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.UnknownStream); break; case Http2FrameType.HEADERS: @@ -5393,6 +5673,7 @@ await WaitForConnectionErrorAsync( CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); break; case Http2FrameType.CONTINUATION: @@ -5410,6 +5691,7 @@ await WaitForConnectionErrorAsync( CoreStrings.FormatHttp2ErrorStreamClosed(Http2FrameType.HEADERS, streamId: 1), CoreStrings.FormatHttp2ErrorStreamHalfClosedRemote(Http2FrameType.HEADERS, streamId: 1) }); + AssertConnectionEndReason(ConnectionEndReason.InvalidStreamId); break; default: throw new NotImplementedException(finalFrameType.ToString()); @@ -5461,6 +5743,20 @@ await WaitForConnectionErrorAsync( 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 +5772,7 @@ await ExpectAsync(Http2FrameType.SETTINGS, withStreamId: 0); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: true); + AssertConnectionNoError(); } [Fact] @@ -5489,6 +5786,7 @@ public async Task StartConnection_SendHttp1xRequest_ReturnHttp11Status400() Assert.NotNull(Http2Connection.InvalidHttp1xErrorResponseBytes); Assert.Equal(Http2Connection.InvalidHttp1xErrorResponseBytes, data); + AssertConnectionEndReason(ConnectionEndReason.InvalidHttpVersion); } [Fact] @@ -5503,6 +5801,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 0, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.Http2ErrorInvalidPreface); + AssertConnectionEndReason(ConnectionEndReason.InvalidHandshake); } [Fact] @@ -5519,6 +5818,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 +5827,7 @@ public async Task StartConnection_SendNothing_NoError() InitializeConnectionWithoutPreface(_noopApplication); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); } [Fact] 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 @@ await ExpectAsync(Http2FrameType.PING, Assert.Equal(KeepAliveState.PingSent, _connection._keepAlive._state); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -68,6 +70,7 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -90,6 +93,7 @@ await ExpectAsync(Http2FrameType.PING, 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 @@ await ExpectAsync(Http2FrameType.PING, TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -165,6 +171,7 @@ await ExpectAsync(Http2FrameType.PING, withStreamId: 0).DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 0, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -195,6 +202,8 @@ await ExpectAsync(Http2FrameType.PING, Assert.Equal(KeepAliveState.Timeout, _connection._keepAlive._state); VerifyGoAway(await ReceiveFrameAsync().DefaultTimeout(), 0, Http2ErrorCode.INTERNAL_ERROR); + + AssertConnectionEndReason(ConnectionEndReason.KeepAliveTimeout); } [Fact] @@ -226,6 +235,7 @@ await ExpectAsync(Http2FrameType.HEADERS, withStreamId: 1).DefaultTimeout(); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -249,6 +259,7 @@ await ExpectAsync(Http2FrameType.HEADERS, TriggerTick(TimeSpan.FromSeconds(1.1)); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -302,6 +313,7 @@ await ExpectAsync(Http2FrameType.HEADERS, // Server could send RST_STREAM await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true).DefaultTimeout(); + AssertConnectionNoError(); } [Fact] @@ -359,5 +371,6 @@ await ExpectAsync(Http2FrameType.HEADERS, // 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..fe3190b2f26a 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 @@ await InitializeConnectionAsync(async context => await StartStreamAsync(1, _browserRequestHeaders, endStream: true); await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + AssertConnectionEndReason(ConnectionEndReason.ErrorWritingHeaders); } [Fact] @@ -2581,6 +2584,7 @@ await ExpectAsync(Http2FrameType.DATA, Assert.Equal("200", _decodedHeaders[InternalHeaderNames.Status]); await WaitForConnectionErrorAsync(ignoreNonGoAwayFrames: false, int.MaxValue, Http2ErrorCode.INTERNAL_ERROR); + AssertConnectionEndReason(ConnectionEndReason.ErrorWritingHeaders); } [Fact] @@ -3774,6 +3778,7 @@ await ExpectAsync(Http2FrameType.CONTINUATION, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: true); + Assert.False(ConnectionTags.ContainsKey(KestrelMetrics.ErrorTypeAttributeName), "Non-error reason shouldn't be added to error.type"); } [Fact] @@ -5807,6 +5812,7 @@ await WaitForConnectionErrorAsync( expectedLastStreamId: 1, expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR, expectedErrorMessage: CoreStrings.BadRequest_MalformedRequestInvalidHeaders); + AssertConnectionEndReason(ConnectionEndReason.InvalidRequestHeaders); } [Fact] @@ -5826,6 +5832,7 @@ await InitializeConnectionAsync(context => 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 546d1337cade..b0a921b13830 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,15 @@ protected static IEnumerable> ReadRateRequestHeader internal TestServiceContext _serviceContext; internal DuplexPipe.DuplexPipePair _pair; + internal IConnectionMetricsTagsFeature _metricsTagsFeature; + internal IConnectionMetricsContextFeature _metricsContextFeature; internal Http2Connection _connection; protected Task _connectionTask; protected long _bytesReceived; + internal Dictionary 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 +425,16 @@ public override void Dispose() base.Dispose(); } + internal void AssertConnectionNoError() + { + MetricsAssert.NoError(ConnectionTags); + } + + internal void AssertConnectionEndReason(ConnectionEndReason expectedEndReason) + { + Assert.Equal(expectedEndReason, MetricsContext.ConnectionEndReason); + } + void IHttpStreamHeadersHandler.OnHeader(ReadOnlySpan name, ReadOnlySpan value) { var nameStr = name.GetHeaderName(); @@ -457,8 +473,14 @@ protected void CreateConnection() _pair = DuplexPipe.CreateConnectionPair(inputPipeOptions, outputPipeOptions); + _metricsTagsFeature = new TestConnectionMetricsTagsFeature(); + + 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); _mockConnectionContext.Setup(x => x.Features).Returns(features); var httpConnectionContext = TestContextFactory.CreateHttpConnectionContext( serviceContext: _serviceContext, @@ -466,7 +488,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 +502,14 @@ 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 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 @@ await ExpectAsync(Http2FrameType.HEADERS, _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 @@ await WaitForConnectionErrorAsyncDoNotCloseTransport c.Abort(It.Is(e => e.Message == CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied)), Times.Once); @@ -425,6 +443,7 @@ await ExpectAsync(Http2FrameType.DATA, 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 @@ await WaitForConnectionErrorAsync( 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 @@ await WaitForConnectionErrorAsync( 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 @@ await WaitForConnectionErrorAsync( 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 @@ await WaitForConnectionErrorAsync( 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 @@ await WaitForConnectionErrorAsync( 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 @@ await WaitForConnectionErrorAsync( 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 @@ await WaitForConnectionErrorAsync( 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 @@ await ExpectAsync(Http2FrameType.DATA, withStreamId: 1); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + AssertConnectionNoError(); _mockTimeoutHandler.VerifyNoOtherCalls(); _mockConnectionContext.VerifyNoOtherCalls(); @@ -962,6 +989,7 @@ await WaitForConnectionErrorAsync( 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..6fc06f49ab90 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,8 @@ await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions reader.Complete(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.InsufficientTlsVersion, m.Tags)); } 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 7bb52e666e03..4471e7db42f3 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3ConnectionTests.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; 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; @@ -187,6 +188,7 @@ public async Task GOAWAY_GracefulServerShutdown_SendsGoAway(int connectionReques Assert.Null(await Http3Api.MultiplexedConnectionContext.AcceptAsync().DefaultTimeout()); await Http3Api.WaitForConnectionStopAsync(expectedStreamId, false, expectedErrorCode: Http3ErrorCode.NoError); + MetricsAssert.NoError(Http3Api.ConnectionTags); } [Fact] @@ -216,6 +218,7 @@ public async Task GOAWAY_GracefulServerShutdownWithActiveRequest_SendsMultipleGo Http3Api.MultiplexedConnectionContext.Abort(); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + MetricsAssert.NoError(Http3Api.ConnectionTags); } [Theory] @@ -242,6 +245,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorCode: Http3ErrorCode.SettingsError, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorControlStreamReservedSetting($"0x{settingIdentifier.ToString("X", CultureInfo.InvariantCulture)}")); + MetricsAssert.Equal(ConnectionEndReason.InvalidSettings, Http3Api.ConnectionTags); } [Theory] @@ -261,6 +265,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorCode: Http3ErrorCode.StreamCreationError, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ControlStreamErrorMultipleInboundStreams(name)); + MetricsAssert.Equal(ConnectionEndReason.StreamCreationError, Http3Api.ConnectionTags); } [Theory] @@ -282,6 +287,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorCode: Http3ErrorCode.UnexpectedFrame, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnControlStream(Http3Formatting.ToFormattedType(f))); + MetricsAssert.Equal(ConnectionEndReason.UnexpectedFrame, Http3Api.ConnectionTags); } [Fact] @@ -306,6 +312,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); + MetricsAssert.Equal(ConnectionEndReason.ClosedCriticalStream, Http3Api.ConnectionTags); } [Fact] @@ -329,6 +336,7 @@ public async Task GOAWAY_TriggersLifetimeNotification_ConnectionClosedRequested( Http3Api.CloseServerGracefully(); await Http3Api.WaitForConnectionStopAsync(0, true, expectedErrorCode: Http3ErrorCode.NoError); + MetricsAssert.NoError(Http3Api.ConnectionTags); } [Fact] @@ -349,6 +357,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorCode: Http3ErrorCode.ClosedCriticalStream, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.Http3ErrorControlStreamClosed); + MetricsAssert.Equal(ConnectionEndReason.ClosedCriticalStream, Http3Api.ConnectionTags); } [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..e1f3b650432a 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 @@ await requestStream.WaitForStreamErrorAsync( expectedErrorMessage: CoreStrings.FormatHttp3StreamErrorFrameReceivedAfterTrailers(Http3Formatting.ToFormattedType(Http3FrameType.Data))); tcs.SetResult(); + + await Http3Api.WaitForConnectionErrorAsync( + ignoreNonGoAwayFrames: false, + expectedLastStreamId: null, + Http3ErrorCode.UnexpectedFrame, + null); + MetricsAssert.Equal(ConnectionEndReason.UnexpectedFrame, Http3Api.ConnectionTags); } [Fact] @@ -2107,6 +2109,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedErrorCode: Http3ErrorCode.UnexpectedFrame, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnRequestStream(Http3Formatting.ToFormattedType(f))); + MetricsAssert.Equal(ConnectionEndReason.UnexpectedFrame, Http3Api.ConnectionTags); } [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); + MetricsAssert.Equal(ConnectionEndReason.UnexpectedFrame, Http3Api.ConnectionTags); } [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..4d9d5a367bb8 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); + MetricsAssert.Equal(ConnectionEndReason.KeepAliveTimeout, Http3Api.ConnectionTags); } [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); + MetricsAssert.Equal(ConnectionEndReason.KeepAliveTimeout, Http3Api.ConnectionTags); } [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); + MetricsAssert.Equal(ConnectionEndReason.KeepAliveTimeout, Http3Api.ConnectionTags); } [Fact] @@ -117,6 +120,7 @@ await Http3Api.InitializeConnectionAsync(_ => Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1)); await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError); + MetricsAssert.Equal(ConnectionEndReason.KeepAliveTimeout, Http3Api.ConnectionTags); } [Fact] @@ -365,6 +369,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedLastStreamId: 4, Http3ErrorCode.InternalError, null); + MetricsAssert.Equal(ConnectionEndReason.MinRequestBodyDataRate, Http3Api.ConnectionTags); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -408,6 +413,7 @@ await Http3Api.WaitForConnectionErrorAsync( Http3ErrorCode.InternalError, matchExpectedErrorMessage: AssertExpectedErrorMessages, expectedErrorMessage: CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied); + MetricsAssert.Equal(ConnectionEndReason.MinResponseDataRate, Http3Api.ConnectionTags); Assert.Contains(TestSink.Writes, w => w.EventId.Name == "ResponseMinimumDataRateNotSatisfied"); } @@ -561,6 +567,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedLastStreamId: null, Http3ErrorCode.InternalError, null); + MetricsAssert.Equal(ConnectionEndReason.MinRequestBodyDataRate, Http3Api.ConnectionTags); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -615,6 +622,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedLastStreamId: null, Http3ErrorCode.InternalError, null); + MetricsAssert.Equal(ConnectionEndReason.MinRequestBodyDataRate, Http3Api.ConnectionTags); _mockTimeoutHandler.VerifyNoOtherCalls(); } @@ -670,6 +678,7 @@ await Http3Api.WaitForConnectionErrorAsync( expectedLastStreamId: null, Http3ErrorCode.InternalError, null); + MetricsAssert.Equal(ConnectionEndReason.MinRequestBodyDataRate, Http3Api.ConnectionTags); _mockTimeoutHandler.VerifyNoOtherCalls(); } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/KeepAliveTimeoutTests.cs index aecd2637f4d3..bb52155311df 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,17 @@ await connection.Send( await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.KeepAliveTimeout, m.Tags)); } [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 +82,17 @@ await connection.Send( } } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [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 +127,17 @@ await connection.Send( await ReceiveResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [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 +172,17 @@ await connection.Send( await ReceiveResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [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 +197,17 @@ private async Task ConnectionTimesOutWhenOpenedButNoRequestSent() await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.KeepAliveTimeout, m.Tags)); } [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 +240,8 @@ await connection.Receive( await connection.Receive("hello, world"); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } 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..0a74f8e059eb 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 @@ await connection.ReceiveEnd( } } + [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.InvalidBodyReaderState)); + }); + } + } + + [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.AppShutdownTimeout)); + }); + } + [Fact] public async Task Http1Connection_IHttpConnectionTagsFeatureIgnoreFeatureSetOnTransport() { @@ -219,6 +384,37 @@ await connection.ReceiveEnd( 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.AppShutdownTimeout))); + } + [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,146 @@ 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"); + Logger.LogInformation("Server aborting request."); + 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 + }, + KeepAlivePingDelay = Timeout.InfiniteTimeSpan + }; + + using var httpClient = new HttpClient(socketsHandler); + + using var httpRequestMessage = new HttpRequestMessage() + { + RequestUri = new Uri("http://localhost/"), + Version = new Version(2, 0), + VersionPolicy = HttpVersionPolicy.RequestVersionExact, + }; + + Logger.LogInformation("Client sending request."); + using var responseMessage = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead); + + Logger.LogInformation("Client validating response status code."); + responseMessage.EnsureSuccessStatusCode(); + + Logger.LogInformation("Client reading response until end."); + var stream = await responseMessage.Content.ReadAsStreamAsync(); + await Assert.ThrowsAnyAsync(() => stream.ReadUntilEndAsync()); + + Logger.LogInformation("Connection waiting for close."); + await connection.WaitForConnectionClose(); + } + + Logger.LogInformation("Asserting metrics."); + 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 +902,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 +913,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 +921,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 +930,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 +939,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 +969,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 +977,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..92b9e8215b69 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 @@ await connection.ReceiveEnd( [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,8 @@ await connection.ReceiveEnd( customApplicationResponse, ""); } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MaxRequestBodySizeExceeded, m.Tags)); } [Fact] @@ -355,6 +362,9 @@ await connection.Receive("HTTP/1.1 101 Switching Protocols", [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 +381,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 +405,8 @@ await connection.ReceiveEnd( Assert.NotNull(requestRejectedEx2); Assert.Equal(CoreStrings.FormatBadRequest_RequestBodyTooLarge(0), requestRejectedEx1.Message); Assert.Equal(CoreStrings.FormatBadRequest_RequestBodyTooLarge(0), requestRejectedEx2.Message); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MaxRequestBodySizeExceeded, m.Tags)); } [Fact] diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestLineSizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestLineSizeTests.cs index 3425eb9a2ee7..3ac2bc361b01 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 connection.Receive( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Theory] @@ -54,7 +59,10 @@ await connection.Receive( [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,13 @@ await connection.ReceiveEnd( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.InvalidRequestLine, m.Tags)); } - 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..b2be002cdd96 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,18 @@ await connection.ReceiveEnd( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MinRequestBodyDataRate, m.Tags)); } [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 +142,18 @@ await connection.ReceiveEnd( 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 => MetricsAssert.Equal(ConnectionEndReason.ServerTimeout, m.Tags)); } [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 +216,7 @@ await connection.ReceiveEnd( "hello, world"); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MinRequestBodyDataRate, m.Tags)); } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeaderLimitsTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestHeaderLimitsTests.cs index a11eff246a0f..d6f687bfd260 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 connection.Receive( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Theory] @@ -56,9 +61,12 @@ await connection.Receive( [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 connection.Receive( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Theory] @@ -82,9 +92,12 @@ await connection.Receive( [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,8 @@ await connection.ReceiveEnd( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MaxRequestHeadersTotalSizeExceeded, m.Tags)); } [Theory] @@ -106,9 +121,12 @@ await connection.ReceiveEnd( [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 +140,8 @@ await connection.ReceiveEnd( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.MaxRequestHeaderCountExceeded, m.Tags)); } private static string MakeHeaders(int count) @@ -138,7 +158,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 +172,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..f5b4283504ef 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,17 @@ await connection.Send( await ReceiveTimeoutResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.RequestHeadersTimeout, m.Tags)); } [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 +80,9 @@ await connection.Send( await ReceiveResponse(connection, testContext); } } + + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Theory] @@ -77,7 +90,10 @@ await connection.Send( [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 +110,17 @@ public async Task ConnectionAbortedWhenRequestLineNotReceivedInTime(string reque await ReceiveTimeoutResponse(connection, testContext); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.RequestHeadersTimeout, m.Tags)); } [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 +144,8 @@ public async Task TimeoutNotResetOnEachRequestLineCharacterReceived() await connection.WaitForConnectionClose(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.RequestHeadersTimeout, m.Tags)); } 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..e8ec0b6152d4 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 @@ await connection.Receive( [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,8 @@ await connection.ReceiveEnd( ""); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.UnexpectedEndOfRequestContent, m.Tags)); } [Fact] @@ -2246,6 +2252,36 @@ await connection.ReceiveEnd( } } + [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 => MetricsAssert.Equal(ConnectionEndReason.TlsNotSupported, m.Tags)); + } + [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 ba363236cb9f..f3ba92d368f6 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 @@ await connection.Receive($"HTTP/1.1 200 OK", [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,8 @@ await connection.Receive($"HTTP/1.1 200 OK", await Assert.ThrowsAsync(() => appTcs.Task).DefaultTimeout(); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.WriteCanceled, m.Tags)); } [Fact] @@ -663,6 +669,9 @@ public async Task AttemptingToWriteZeroContentLengthFor2xxResponsesOnConnect_Con private async Task AttemptingToWriteNonzeroContentLengthFails(int statusCode, HttpMethod method) { + var testMeterFactory = new TestMeterFactory(); + using var connectionDuration = new MetricCollector(testMeterFactory, "Microsoft.AspNetCore.Server.Kestrel", "kestrel.connection.duration"); + var responseWriteTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using (var server = new TestServer(async httpContext => @@ -681,7 +690,7 @@ private async Task AttemptingToWriteNonzeroContentLengthFails(int statusCode, Ht } responseWriteTcs.TrySetResult(); - }, new TestServiceContext(LoggerFactory))) + }, new TestServiceContext(LoggerFactory, metrics: new KestrelMetrics(testMeterFactory)))) { using (var connection = server.CreateConnection()) { @@ -695,6 +704,8 @@ await connection.Send( Assert.Equal(CoreStrings.FormatHeaderNotAllowedOnResponse("Content-Length", statusCode), ex.Message); } } + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } private async Task AttemptingToWriteZeroContentLength_ContentLengthRemoved(int statusCode, HttpMethod method) @@ -935,7 +946,10 @@ await connection.Receive( [Fact] public async Task ThrowsAndClosesConnectionWhenAppWritesMoreThanContentLengthWrite() { - 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)) { ServerOptions = { AllowSynchronousIO = true } }; @@ -970,12 +984,16 @@ await connection.Receive( Assert.Equal( $"Response Content-Length mismatch: too many bytes written (12 of 11).", logMessage.Exception.Message); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.ResponseContentLengthMismatch, m.Tags)); } [Fact] public async Task ThrowsAndClosesConnectionWhenAppWritesMoreThanContentLengthWriteAsync() { - 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)); await using (var server = new TestServer(async httpContext => { @@ -1004,6 +1022,8 @@ await connection.ReceiveEnd( Assert.Equal( $"Response Content-Length mismatch: too many bytes written (12 of 11).", logMessage.Exception.Message); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.ResponseContentLengthMismatch, m.Tags)); } [Fact] @@ -2395,7 +2415,9 @@ await connection.ReceiveEnd( [Fact] public async Task ThrowingResultsIn500Response() { - 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)); bool onStartingCalled = false; @@ -2440,6 +2462,8 @@ await connection.ReceiveEnd( Assert.False(onStartingCalled); Assert.Equal(2, LogMessages.Where(message => message.LogLevel == LogLevel.Error).Count()); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Fact] @@ -2594,7 +2618,9 @@ await connection.Receive( [Fact] public async Task ThrowingInOnCompletedIsLogged() { - 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 onCompletedCalled1 = false; var onCompletedCalled2 = false; @@ -2638,12 +2664,16 @@ await connection.Receive( Assert.Equal(2, LogMessages.Where(message => message.LogLevel == LogLevel.Error).Count()); Assert.True(onCompletedCalled1); Assert.True(onCompletedCalled2); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.NoError(m.Tags)); } [Fact] public async Task ThrowingAfterWritingKillsConnection() { - 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)); bool onStartingCalled = false; @@ -2679,12 +2709,16 @@ await connection.ReceiveEnd( Assert.True(onStartingCalled); Assert.Single(LogMessages, message => message.LogLevel == LogLevel.Error); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.ErrorAfterStartingResponse, m.Tags)); } [Fact] public async Task ThrowingAfterPartialWriteKillsConnection() { - 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)); bool onStartingCalled = false; @@ -2720,6 +2754,8 @@ await connection.ReceiveEnd( Assert.True(onStartingCalled); Assert.Single(LogMessages, message => message.LogLevel == LogLevel.Error); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.ErrorAfterStartingResponse, m.Tags)); } [Fact] @@ -2779,7 +2815,9 @@ await connection.Send( [Fact] public async Task AppAbortIsLogged() { - 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 = new TestServer(httpContext => { @@ -2799,6 +2837,8 @@ await connection.Send( } Assert.Single(LogMessages.Where(m => m.Message.Contains(CoreStrings.ConnectionAbortedByApplication))); + + Assert.Collection(connectionDuration.GetMeasurementSnapshot(), m => MetricsAssert.Equal(ConnectionEndReason.AbortedByApp, m.Tags)); } [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..4066e1592550 100644 --- a/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj +++ b/src/Servers/Kestrel/test/Sockets.FunctionalTests/Sockets.FunctionalTests.csproj @@ -23,6 +23,7 @@ + @@ -33,6 +34,7 @@ + diff --git a/src/Shared/Metrics/MetricsExtensions.cs b/src/Shared/Metrics/MetricsExtensions.cs index 307bb9517601..9b1e8bba0f07 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,54 @@ 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[i] = new KeyValuePair(name, value); + break; + } + } + } + else + { + foreach (var tag in tags) + { + if (tag.Key == name) + { + tags.Remove(tag); + tags.Add(new KeyValuePair(name, value)); + break; + } + } + } + } + + 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..4a8f2e4894c3 --- /dev/null +++ b/src/Shared/ServerInfrastructure/Http2/ConnectionEndReason.cs @@ -0,0 +1,59 @@ +// 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, + OtherError, + ConnectionReset, + FlowControlWindowExceeded, + KeepAliveTimeout, + InsufficientTlsVersion, + InvalidHandshake, + InvalidStreamId, + FrameAfterStreamClose, + UnknownStream, + UnexpectedFrame, + InvalidFrameLength, + InvalidDataPadding, + InvalidRequestHeaders, + InvalidRequestLine, + StreamResetLimitExceeded, + InvalidWindowUpdateSize, + StreamSelfDependency, + InvalidSettings, + MissingStreamEnd, + MaxFrameLengthExceeded, + ErrorReadingHeaders, + ErrorWritingHeaders, + InvalidHttpVersion, + RequestHeadersTimeout, + MinRequestBodyDataRate, + MinResponseDataRate, + FlowControlQueueSizeExceeded, + OutputQueueSizeExceeded, + ClosedCriticalStream, + AbortedByApp, + WriteCanceled, + InvalidBodyReaderState, + ServerTimeout, + StreamCreationError, + IOError, + ClientGoAway, + AppShutdownTimeout, + GracefulAppShutdown, + TransportCompleted, + TlsHandshakeFailed, + TlsNotSupported, + MaxRequestBodySizeExceeded, + UnexpectedEndOfRequestContent, + MaxConcurrentConnectionsExceeded, + MaxRequestHeadersTotalSizeExceeded, + MaxRequestHeaderCountExceeded, + ResponseContentLengthMismatch, + RequestNoKeepAlive, + ResponseNoKeepAlive, + ErrorAfterStartingResponse +} 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();