diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs index 07d723e4da871debda50fb83b4fc327cf3b586bf..ea0bb16a043224e43d6a202274c47349983b8f63 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs @@ -486,8 +486,15 @@ namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests var response = await deploymentResult.HttpClient.GetAsync("/Abort").TimeoutAfter(TimeoutExtensions.DefaultTimeoutValue); Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + +#if NEWSHIM_FUNCTIONALS + // In-proc SocketConnection isn't used and there's no abort // 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync()); +#else + // 0x80072efe ERROR_INTERNET_CONNECTION_ABORTED The connection with the server was terminated abnormally + Assert.Contains("0x80072efe", await response.Content.ReadAsStringAsync()); +#endif } catch (HttpRequestException) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index ad1ffff50e52aa9becc9d5a57c9afecd2500d0ef..2a5927b901e61fb09cdbadfc1650e908890e86d5 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -93,7 +93,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http _http1Output.Dispose(); } - public void OnInputOrOutputCompleted() + void IRequestProcessor.OnInputOrOutputCompleted() + { + // Closed gracefully. + _http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); + CancelRequestAbortedToken(); + } + + void IHttpOutputAborter.OnInputOrOutputCompleted() { _http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); CancelRequestAbortedToken(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index 5a3051ae56c8681839352df922571b227c436309..b9711909eb76fe64f5b283541cb79afcc66dc81a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http // so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400 // response is written after observing the unexpected end of request content instead of just // closing the connection without a response as expected. - _context.OnInputOrOutputCompleted(); + ((IHttpOutputAborter)_context).OnInputOrOutputCompleted(); KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index b197c7fccb28caac5ee02994674d6e482978700e..ac3e9d166d4d9e54f0ccbbdd9c4ec9b3c8d2a194 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -150,7 +150,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2 public void OnInputOrOutputCompleted() { TryClose(); - _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0; + _frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); } public void Abort(ConnectionAbortedException ex) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index cf930c744cbb60bd83c8604c1d464b5d65f3178a..f732302c251fe0ddad711544ebf93faecb666cb6 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -29,9 +29,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// </summary> public class KestrelServerOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static KestrelServerOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged. internal static readonly Func<string, Encoding?> DefaultHeaderEncodingSelector = _ => null; + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } = _finOnError; + private Func<string, Encoding?> _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector; private Func<string, Encoding?> _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector; diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs index 60966046a3a36a06aef433707f055853987cc4e8..260657cff99b4aafda3debd13cd3b525bd3856b4 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal void ConnectionWriteFin(string connectionId, string reason); + void ConnectionWriteRst(string connectionId, string reason); + void ConnectionWrite(string connectionId, int count); void ConnectionWriteCallback(string connectionId, int status); diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs index fdfc852ba243178b2a4598cbb34596c53b1c97f9..ca163d6d2aff4df77a71bd9320c25ecdf7e13d55 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs @@ -6,6 +6,8 @@ using System.Buffers; using System.IO; using System.IO.Pipelines; using System.Net; +using System.Net.Sockets; +using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -28,6 +30,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private readonly IDuplexPipe _originalTransport; private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); + private readonly bool _finOnError; + private volatile ConnectionAbortedException _abortReason; private MemoryHandle _bufferHandle; @@ -43,9 +47,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal PipeOptions inputOptions = null, PipeOptions outputOptions = null, long? maxReadBufferSize = null, - long? maxWriteBufferSize = null) + long? maxWriteBufferSize = null, + bool finOnError = false) { _socket = socket; + _finOnError = finOnError; LocalEndPoint = localEndPoint; RemoteEndPoint = remoteEndPoint; @@ -124,6 +130,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { inputError ??= _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully."); + if (!_finOnError && _abortReason is not null) + { + // When shutdown isn't clean (note that we're using _abortReason, rather than inputError, to exclude that case), + // we set the DontLinger socket option to cause libuv to send a RST and release any buffered response data. + SetDontLingerOption(_socket); + } + // Now, complete the input so that no more reads can happen Input.Complete(inputError); Output.Complete(outputError); @@ -132,8 +145,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal // on the stream handle Input.CancelPendingFlush(); - // Send a FIN - Log.ConnectionWriteFin(ConnectionId, inputError.Message); + if (!_finOnError && _abortReason is not null) + { + // Send a RST + Log.ConnectionWriteRst(ConnectionId, inputError.Message); + } + else + { + // Send a FIN + Log.ConnectionWriteFin(ConnectionId, inputError.Message); + } // We're done with the socket now _socket.Dispose(); @@ -150,6 +171,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } } + /// <remarks> + /// This should be called on <see cref="_socket"/> before it is disposed. + /// Both <see cref="Abort"/> and <see cref="StartCore"/> call dispose but, rather than predict + /// which will do so first (which varies), we make this method idempotent and call it in both. + /// </remarks> + private static void SetDontLingerOption(UvStreamHandle socket) + { + if (!socket.IsClosed && !socket.IsInvalid) + { + var libuv = socket.Libuv; + var pSocket = IntPtr.Zero; + libuv.uv_fileno(socket, ref pSocket); + + // libuv doesn't expose setsockopt, so we take advantage of the fact that + // Socket already has a PAL + using var managedHandle = new SafeSocketHandle(pSocket, ownsHandle: false); + using var managedSocket = new Socket(managedHandle); + managedSocket.LingerState = new LingerOption(enable: true, seconds: 0); + } + } + public override void Abort(ConnectionAbortedException abortReason) { _abortReason = abortReason; @@ -157,8 +199,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal // Cancel WriteOutputAsync loop after setting _abortReason. Output.CancelPendingRead(); - // This cancels any pending I/O. - Thread.Post(s => s.Dispose(), _socket); + Thread.Post(static (self) => + { + if (!self._finOnError) + { + SetDontLingerOption(self._socket); + } + + // This cancels any pending I/O. + self._socket.Dispose(); + }, this); } public override async ValueTask DisposeAsync() diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs index bdfcc2e65b58c9c15faeee676999c5d13bb3faf4..f03f8e603097ec36940e2daebb405fa337c61d17 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Threading; @@ -134,9 +135,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal { // TODO: Move thread management to LibuvTransportFactory // TODO: Split endpoint management from thread management + + // When `FinOnError` is false (the default), we need to be able to forcibly abort connections. + // On Windows, libuv 1.10.0 will call `shutdown`, preventing forcible abort, on any socket + // not flagged as `UV_HANDLE_SHARED_TCP_SOCKET`. The only way we've found to cause socket + // to be flagged as `UV_HANDLE_SHARED_TCP_SOCKET` is to share it across a named pipe (which + // must, itself, be flagged `ipc`), which naturally happens when a `ListenerPrimary` dispatches + // a connection to a `ListenerSecondary`. Therefore, in scenarios where this is required, we + // tell the `ListenerPrimary` to dispatch *all* connections to secondary and create an + // additional `ListenerSecondary` to replace the lost capacity. + var dispatchAllToSecondary = Libuv.IsWindows && !TransportContext.Options.FinOnError; + #pragma warning disable CS0618 - for (var index = 0; index < TransportOptions.ThreadCount; index++) + var threadCount = dispatchAllToSecondary + ? TransportOptions.ThreadCount + 1 + : TransportOptions.ThreadCount; #pragma warning restore CS0618 + + for (var index = 0; index < threadCount; index++) { Threads.Add(new LibuvThread(Libuv, TransportContext)); } @@ -148,10 +164,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal try { -#pragma warning disable CS0618 - if (TransportOptions.ThreadCount == 1) -#pragma warning restore CS0618 + if (threadCount == 1) { + Debug.Assert(!dispatchAllToSecondary, "Should have taken the primary/secondary code path"); + var listener = new Listener(TransportContext); _listeners.Add(listener); await listener.StartAsync(EndPoint, Threads[0]).ConfigureAwait(false); @@ -162,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); - var listenerPrimary = new ListenerPrimary(TransportContext); + var listenerPrimary = new ListenerPrimary(TransportContext, dispatchAllToSecondary); _listeners.Add(listenerPrimary); await listenerPrimary.StartAsync(pipeName, pipeMessage, EndPoint, Threads[0]).ConfigureAwait(false); EndPoint = listenerPrimary.EndPoint; diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs index f4e4d64aafff8b844f0ae1a1f19884c673fef7b7..c360efc7bf93f11809c6a258c83a41ee00f76871 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs @@ -27,6 +27,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal [LoggerMessage(7, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending FIN because: ""{Reason}""", EventName = nameof(ConnectionWriteFin))] public partial void ConnectionWriteFin(string connectionId, string reason); + [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = nameof(ConnectionWriteRst))] + public partial void ConnectionWriteRst(string connectionId, string reason); + public void ConnectionWrite(string connectionId, int count) { // Don't log for now since this could be *too* verbose. diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs index 7ad346f6ea59064a90d98bdd48d5111c32460fd9..8ab2f5f34503fdcd02c0a888cc6d9b76be61bc23 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs @@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal var options = TransportContext.Options; #pragma warning disable CS0618 - var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize); + var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize, options.FinOnError); #pragma warning restore CS0618 connection.Start(); diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs index 8a899c91c44ebe3836f1a4710dcf290715d5d9cc..18ae24af5adb6dc51af6d1c68bc81340e3ce6b71 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net; using System.Runtime.InteropServices; @@ -22,6 +23,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal private readonly List<UvPipeHandle> _dispatchPipes = new List<UvPipeHandle>(); // The list of pipes we've created but may not be part of _dispatchPipes private readonly List<UvPipeHandle> _createdPipes = new List<UvPipeHandle>(); + + // If true, dispatch all connections to _dispatchPipes - don't process any in the primary + private readonly bool _dispatchAll; + private int _dispatchIndex; private string _pipeName; private byte[] _pipeMessage; @@ -32,8 +37,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal // but it has no other functional significance private readonly ArraySegment<ArraySegment<byte>> _dummyMessage = new ArraySegment<ArraySegment<byte>>(new[] { new ArraySegment<byte>(new byte[] { 1, 2, 3, 4 }) }); - public ListenerPrimary(LibuvTransportContext transportContext) : base(transportContext) + public ListenerPrimary(LibuvTransportContext transportContext, bool dispatchAll) : base(transportContext) { + _dispatchAll = dispatchAll; } /// <summary> @@ -107,9 +113,27 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal protected override void DispatchConnection(UvStreamHandle socket) { - var index = _dispatchIndex++ % (_dispatchPipes.Count + 1); + var modulus = _dispatchAll ? _dispatchPipes.Count : (_dispatchPipes.Count + 1); + if (modulus == 0) + { + if (_createdPipes.Count == 0) + { +#pragma warning disable CS0618 // Type or member is obsolete + Log.LogError(0, $"Connection received before listeners were initialized - see https://aka.ms/dotnet/aspnet/finonerror for possible mitigations"); +#pragma warning restore CS0618 // Type or member is obsolete + } + else + { + Log.LogError(0, "Unable to process connection since listeners failed to initialize - see https://aka.ms/dotnet/aspnet/finonerror for possible mitigations"); + } + + return; + } + + var index = _dispatchIndex++ % modulus; if (index == _dispatchPipes.Count) { + Debug.Assert(!_dispatchAll, "Should have dispatched to a secondary listener"); base.DispatchConnection(socket); } else diff --git a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs index 142a2f99c4cefe7c0c083c47f8cb60420a28b768..9e58ac1a2d7ad7c0e988f0deea5b869e2cbf21ff 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs @@ -12,6 +12,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv [Obsolete("The libuv transport is obsolete and will be removed in a future release. See https://aka.ms/libuvtransport for details.", error: false)] // Remove after .NET 6. public class LibuvTransportOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static LibuvTransportOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + + // Opt-out flag for back compat. Remove in 7.0. + internal bool FinOnError { get; set; } = _finOnError; + /// <summary> /// The number of libuv I/O threads used to process requests. /// </summary> diff --git a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs index 82d04cd1ac14b1cf0e1da96ba94db1c456a236a1..58d57451490e295d2e7c7ba39fe2ec3648486297 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs @@ -5,11 +5,14 @@ using System; using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; @@ -33,7 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests // Start primary listener var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); - var listenerPrimary = new ListenerPrimary(transportContextPrimary); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false); await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); var address = GetUri(listenerPrimary.EndPoint); @@ -50,6 +53,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests } var listenerCount = listenerPrimary.UvPipeCount; + Assert.Equal(0, listenerCount); + // Add secondary listener var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); await libuvThreadSecondary.StartAsync(); @@ -85,6 +90,74 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests await listenerPrimary.DisposeAsync(); await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public async Task ConnectionsGetRoundRobinedToSecondaryListeners_DispatchAll(int secondaryCount) + { + var libuv = new LibuvFunctions(); + + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var transportContextPrimary = new TestLibuvTransportContext(); + var transportContextSecondary = new TestLibuvTransportContext(); + + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + // Start primary listener + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); + await libuvThreadPrimary.StartAsync(); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); + + Assert.Equal(0, listenerPrimary.UvPipeCount); + + // Add secondary listeners + var listenerSecondaries = new ListenerSecondary[secondaryCount]; + for (int i = 0; i < secondaryCount; i++) + { + var libuvThread = new LibuvThread(libuv, transportContextSecondary); + await libuvThread.StartAsync(); + var listener = new ListenerSecondary(transportContextSecondary); + await listener.StartAsync(pipeName, pipeMessage, endpoint, libuvThread); + listenerSecondaries[i] = listener; + } + + var maxWait = Task.Delay(TestConstants.DefaultTimeout); + // wait for ListenerPrimary.ReadCallback to add the secondary pipe + while (listenerPrimary.UvPipeCount < secondaryCount) + { + var completed = await Task.WhenAny(maxWait, Task.Delay(100)); + if (ReferenceEquals(completed, maxWait)) + { + throw new TimeoutException("Timed out waiting for secondary listener to become available"); + } + } + + // Check that the secondaries are visited in order and that it wraps + // around without hitting the primary + for (int i = 0; i < secondaryCount + 1; i++) + { + var expectedTask = listenerSecondaries[i % secondaryCount].AcceptAsync().AsTask(); + + using var socket = await HttpClientSlim.GetSocket(address); + + await using var connection = await expectedTask.DefaultTimeout(); + } + + foreach (var listenerSecondary in listenerSecondaries) + { + var libuvThread = listenerSecondary.Thread; + await listenerSecondary.DisposeAsync(); + await libuvThread.StopAsync(TimeSpan.FromSeconds(5)); + } + + await listenerPrimary.DisposeAsync(); + await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); + } // https://github.com/aspnet/KestrelHttpServer/issues/1182 [Fact] @@ -103,7 +176,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests // Start primary listener var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); - var listenerPrimary = new ListenerPrimary(transportContextPrimary); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false); await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); var address = GetUri(listenerPrimary.EndPoint); @@ -181,7 +254,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests Assert.Equal(LogLevel.Debug, logMessage.LogLevel); } - [Fact] public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() { @@ -199,7 +271,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests // Start primary listener var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); - var listenerPrimary = new ListenerPrimary(transportContextPrimary); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false); await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); var address = GetUri(listenerPrimary.EndPoint); @@ -235,6 +307,108 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests Assert.Contains("Bad data", errorMessage.Exception.ToString()); } + [Fact] + public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored_DispatchAllNoneRemaining() + { + var libuv = new LibuvFunctions(); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var logger = new TestApplicationErrorLogger(); + + var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) }; + var transportContextSecondary = new TestLibuvTransportContext(); + + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + // Start primary listener + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); + await libuvThreadPrimary.StartAsync(); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); + + // Add secondary listener with wrong pipe message + var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); + await libuvThreadSecondary.StartAsync(); + var listenerSecondary = new ListenerSecondary(transportContextSecondary); + await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), endpoint, libuvThreadSecondary); + + // Wait up to 10 seconds for error to be logged + for (var i = 0; i < 10 && logger.TotalErrorsLogged == 0; i++) + { + await Task.Delay(100); + } + + Assert.Equal(1, logger.TotalErrorsLogged); + + var badDataMessage = Assert.Single(logger.Messages.Where(m => m.LogLevel == LogLevel.Error)); + Assert.IsType<IOException>(badDataMessage.Exception); + Assert.Contains("Bad data", badDataMessage.Exception.ToString()); + + using var socket = await HttpClientSlim.GetSocket(address); + + var _ = listenerPrimary.AcceptAsync(); + + // Wait up to 10 seconds for error to be logged + for (var i = 0; i < 10 && logger.TotalErrorsLogged <= 1; i++) + { + await Task.Delay(100); + } + + var noSecondariesMessage = logger.Messages.Last(m => m.LogLevel == LogLevel.Error); + Assert.Null(noSecondariesMessage.Exception); + Assert.Contains("listeners failed to initialize", noSecondariesMessage.Message); + + Assert.Null(libuvThreadPrimary.FatalError); + + await listenerSecondary.DisposeAsync(); + await libuvThreadSecondary.StopAsync(TimeSpan.FromSeconds(5)); + + await listenerPrimary.DisposeAsync(); + await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); + } + + [Fact] + public async Task DispatchAllConnectionBeforeSecondaries() + { + var libuv = new LibuvFunctions(); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var logger = new TestApplicationErrorLogger(); + + var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) }; + var transportContextSecondary = new TestLibuvTransportContext(); + + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + // Start primary listener + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); + await libuvThreadPrimary.StartAsync(); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); + + using var socket = await HttpClientSlim.GetSocket(address); + + var _ = listenerPrimary.AcceptAsync(); + + // Wait up to 10 seconds for error to be logged + for (var i = 0; i < 10 && logger.TotalErrorsLogged <= 1; i++) + { + await Task.Delay(100); + } + + var noSecondariesMessage = logger.Messages.Last(m => m.LogLevel == LogLevel.Error); + Assert.Null(noSecondariesMessage.Exception); + Assert.Contains("before listeners", noSecondariesMessage.Message); + + Assert.Null(libuvThreadPrimary.FatalError); + + await listenerPrimary.DisposeAsync(); + await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); + } private static async Task AssertRoundRobin(Uri address, ListenerPrimary listenerPrimary, ListenerSecondary listenerSecondary, ListenerContext currentListener, Task<LibuvConnection> expected = null, int connections = 4) { diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs index f5e5c49637865cebe419c7f9697507704453a53e..cdd82f481e8f243d4fdc36f86f8b21f1d971b726 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal void ConnectionReadFin(SocketConnection connection); void ConnectionWriteFin(SocketConnection connection, string reason); + void ConnectionWriteRst(SocketConnection connection, string reason); void ConnectionError(SocketConnection connection, Exception ex); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index f43c2e1454f1462aaaebb4e8505e6ad3c003969d..c7f58a128a82586bba1545571cc7f683d1509927 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { + internal sealed partial class SocketConnection : TransportConnection { private static readonly int MinAllocBufferSize = PinnedBlockMemoryPool.BlockSize / 2; @@ -30,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource(); private bool _connectionClosed; private readonly bool _waitForData; + private readonly bool _finOnError; internal SocketConnection(Socket socket, MemoryPool<byte> memoryPool, @@ -38,7 +40,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal SocketSenderPool socketSenderPool, PipeOptions inputOptions, PipeOptions outputOptions, - bool waitForData = true) + bool waitForData = true, + bool finOnError = false) { Debug.Assert(socket != null); Debug.Assert(memoryPool != null); @@ -49,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal _trace = trace; _waitForData = waitForData; _socketSenderPool = socketSenderPool; + _finOnError = finOnError; LocalEndPoint = _socket.LocalEndPoint; RemoteEndPoint = _socket.RemoteEndPoint; @@ -329,11 +333,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal // ever observe the nondescript ConnectionAbortedException except for connection middleware attempting // to half close the connection which is currently unsupported. _shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully."); + + // NB: not _shutdownReason since we don't want to do this on graceful completion + if (!_finOnError && shutdownReason is not null) + { + _trace.ConnectionWriteRst(this, shutdownReason.Message); + + // This forces an abortive close with linger time 0 (and implies Dispose) + _socket.Close(timeout: 0); + return; + } + _trace.ConnectionWriteFin(this, _shutdownReason.Message); try { - // Try to gracefully close the socket even for aborts to match libuv behavior. _socket.Shutdown(SocketShutdown.Both); } catch diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs index 1a48fa189a15526118c9a569acb64e4298c98526..f4108eae678a651fc44b73b033b1fd5952c519b6 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs @@ -43,6 +43,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal } } + [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = "ConnectionWriteRst", SkipEnabledCheck = true)] + private static partial void ConnectionWriteRst(ILogger logger, string connectionId, string reason); + + public void ConnectionWriteRst(SocketConnection connection, string reason) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + ConnectionWriteRst(_logger, connection.ConnectionId, reason); + } + } + public void ConnectionWrite(SocketConnection connection, int count) { // Don't log for now since this could be *too* verbose. diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index 71f3cdc0fc0cc6544059863505d7a1024a9061ba..2635af9792d7e9e4a683aad7b1721241e87c4f46 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -104,7 +104,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets setting.SocketSenderPool, setting.InputOptions, setting.OutputOptions, - waitForData: _options.WaitForDataBeforeAllocatingBuffer); + waitForData: _options.WaitForDataBeforeAllocatingBuffer, + finOnError: _options.FinOnError); connection.Start(); return connection; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index d0c88c0535f3a1106f962c41f7b93624bfe91fc3..fec0b3bef938afd9efdb16bbe09ec4868319a371 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -23,8 +23,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets MaxWriteBufferSize = transportOptions.MaxWriteBufferSize; UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling; MemoryPoolFactory = transportOptions.MemoryPoolFactory; + FinOnError = transportOptions.FinOnError; } + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } + /// <summary> /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool. /// </summary> diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 9130cb98045407c6f3952e2185b5436dc3a9aae8..4f22c079dc1fe5cfea2577145960bde5ac3913c3 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -13,6 +13,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets /// </summary> public class SocketTransportOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static SocketTransportOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } = _finOnError; + /// <summary> /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool. /// </summary> diff --git a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs index 5dd91c4337d9cddb13d344fd4589fdce336fd25d..eb4f310e7a291559b94ab733c20529822832c993 100644 --- a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs +++ b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Testing public async Task WaitForConnectionClose() { var buffer = new byte[128]; - var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout); + var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).ContinueWith(t => t.IsFaulted ? 0 : t.Result).TimeoutAfter(Timeout); if (bytesTransferred > 0) { diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 79fc65849640474021c3cf81095c56be05e84097..0c8109cb99522b601a195ddafb2aee1b5d494f5e 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { } - public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions) + public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions, Action<IServiceCollection> configureServices = null) : this(app, context, options => { var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) @@ -57,7 +57,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests }; configureListenOptions(listenOptions); options.CodeBackedListenOptions.Add(listenOptions); - }, _ => { }) + }, s => + { + configureServices?.Invoke(s); + }) { } diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 1a12f4629e444cf49cc96b2cf0605b94afb51173..ff5f06ea21a51fa6d30b4cb65f92163fffb30588 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -46,6 +46,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2 }; } + [ConditionalFact] + public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN() + { + var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + TestSink.MessageLogged += context => + { + if (context.EventId.Name == "Http2ConnectionClosed") + { + connectionClosed.SetResult(); + } + else if (context.EventId.Name == "ConnectionReadFin") + { + readFin.SetResult(); + } + else if (context.EventId.Name == "ConnectionWriteFin") + { + writeFin.SetResult(); + } + }; + + var testContext = new TestServiceContext(LoggerFactory); + + testContext.InitializeHeartbeat(); + + await using (var server = new TestServer(context => + { + return context.Response.WriteAsync("hello world " + context.Request.Protocol); + }, + testContext, + kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(_x509Certificate2); + }); + })) + { + var response = await Client.GetStringAsync($"https://localhost:{server.Port}/"); + Assert.Equal("hello world HTTP/2", response); + Client.Dispose(); // Close the socket, no GoAway is sent. + + await readFin.Task.DefaultTimeout(); + await writeFin.Task.DefaultTimeout(); + await connectionClosed.Task.DefaultTimeout(); + + await server.StopAsync(); + } + } + [CollectDump] [ConditionalFact] public async Task GracefulShutdownWaitsForRequestsToFinish() diff --git a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs index e10a5dc3ad3d75a2fff50b15e0b17628ffce0bfb..af391389ade33d83d8d6d35d194db87dbae97560 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs @@ -21,7 +21,13 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; +#if LIBUV +using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv; +#elif SOCKETS +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +#endif using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; @@ -40,6 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public class RequestTests : LoggedTest { private const int _connectionStartedEventId = 1; + private const int _connectionReadFinEventId = 6; private const int _connectionResetEventId = 19; private static readonly int _semaphoreWaitTimeout = Debugger.IsAttached ? 10000 : 2500; @@ -234,6 +241,59 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } + [Fact] + public async Task ConnectionClosedPriorToRequestIsLoggedAsDebug() + { + var connectionStarted = new SemaphoreSlim(0); + var connectionReadFin = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + TestSink.MessageLogged += context => + { + if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Connections" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") + { + return; + } + + if (context.EventId.Id == _connectionStartedEventId) + { + connectionStarted.Release(); + } + else if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + + if (context.LogLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + // Wait until connection is established + Assert.True(await connectionStarted.WaitAsync(TestConstants.DefaultTimeout)); + + connection.ShutdownSend(); + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout)); + await connection.ReceiveEnd(); + } + } + + Assert.False(loggedHigherThanDebug); + } + [Fact] public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() { @@ -286,6 +346,66 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.False(loggedHigherThanDebug); } + [Fact] + public async Task ConnectionClosedBetweenRequestsIsLoggedAsDebug() + { + var connectionReadFin = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + TestSink.MessageLogged += context => + { + if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") + { + return; + } + + if (context.LogLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + + if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + // Make sure the response is fully received, so a write failure (e.g. EPIPE) doesn't cause + // a more critical log message. + await connection.Receive( + "HTTP/1.1 200 OK", + "Content-Length: 0", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + connection.ShutdownSend(); + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout)); + + await connection.ReceiveEnd(); + } + } + + Assert.False(loggedHigherThanDebug); + } + [Fact] public async Task ConnectionResetBetweenRequestsIsLoggedAsDebug() { @@ -345,10 +465,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.False(loggedHigherThanDebug); } - [Fact] - public async Task ConnectionResetMidRequestIsLoggedAsDebug() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedOrResetMidRequestIsLoggedAsDebug(bool close) { var requestStarted = new SemaphoreSlim(0); + var connectionReadFin = new SemaphoreSlim(0); var connectionReset = new SemaphoreSlim(0); var connectionClosing = new SemaphoreSlim(0); var loggedHigherThanDebug = false; @@ -367,6 +490,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests loggedHigherThanDebug = true; } + if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + if (context.EventId.Id == _connectionResetEventId) { connectionReset.Release(); @@ -387,15 +515,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests // Wait until connection is established Assert.True(await requestStarted.WaitAsync(TestConstants.DefaultTimeout), "request should have started"); - connection.Reset(); - } + if (close) + { + connection.ShutdownSend(); + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout), "Connection close event should have been logged"); + } + else + { + connection.Reset(); - // If the reset is correctly logged as Debug, the wait below should complete shortly. - // This check MUST come before disposing the server, otherwise there's a race where the RST - // is still in flight when the connection is aborted, leading to the reset never being received - // and therefore not logged. - Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); - connectionClosing.Release(); + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); + } + connectionClosing.Release(); + } } Assert.False(loggedHigherThanDebug, "Logged event should not have been higher than debug."); @@ -494,18 +630,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Fact] - public async Task AbortingTheConnectionSendsFIN() + [Theory] +#if LIBUV + [InlineData(true, 1)] + [InlineData(false, 1)] + [InlineData(true, 2)] + [InlineData(false, 2)] + public async Task AbortingTheConnection(bool fin, int threadCount) +#else + [InlineData(true)] + [InlineData(false)] + public async Task AbortingTheConnection(bool fin) +#endif { + var connectionAborted = new SemaphoreSlim(0); + var builder = TransportSelector.GetHostBuilder() +#if LIBUV + .ConfigureServices(services => + { +#pragma warning disable CS0618 // Type or member is obsolete + services.Configure<LibuvTransportOptions>(options => + { + options.ThreadCount = threadCount; + }); +#pragma warning restore CS0618 // Type or member is obsolete + }) +#endif .ConfigureWebHost(webHostBuilder => { webHostBuilder + .ConfigureServices(s => SetFinOnError(s, fin)) .UseKestrel() .UseUrls("http://127.0.0.1:0") .Configure(app => app.Run(context => { context.Abort(); + connectionAborted.Release(); return Task.CompletedTask; })); }) @@ -519,8 +680,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); - int result = socket.Receive(new byte[32]); - Assert.Equal(0, result); + + Assert.True(await connectionAborted.WaitAsync(_semaphoreWaitTimeout)); + + if (fin) + { + int result = socket.Receive(new byte[32]); + Assert.Equal(0, result); + } + else + { + Assert.Throws<SocketException>(() => socket.Receive(new byte[32])); + } } await host.StopAsync(); @@ -731,16 +902,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } [Theory] - [MemberData(nameof(ConnectionMiddlewareDataName))] - public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName) + [InlineData("Loopback", true)] + [InlineData("PassThrough", true)] + [InlineData("Loopback", false)] + [InlineData("PassThrough", false)] + public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName, bool fin) { const int connectionPausedEventId = 4; const int connectionFinSentEventId = 7; + const int connectionRstSentEventId = 8; const int maxRequestBufferSize = 4096; var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var serverClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverFinConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverRstConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => @@ -757,7 +933,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } else if (context.EventId == connectionFinSentEventId) { - serverClosedConnection.SetResult(); + serverFinConnection.SetResult(); + } + else if (context.EventId == connectionRstSentEventId) + { + serverRstConnection.SetResult(); } }; @@ -766,6 +946,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { ServerOptions = { + FinOnError = fin, Limits = { MaxRequestBufferSize = maxRequestBufferSize, @@ -783,10 +964,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests context.Abort(); - await serverClosedConnection.Task; + if (fin) + { + await serverFinConnection.Task.DefaultTimeout(); + } + else + { + await serverRstConnection.Task.DefaultTimeout(); + } appFuncCompleted.SetResult(); - }, testContext, ConnectionMiddlewareData[listenOptionsName]())) + }, testContext, listen => + { + if (listenOptionsName == "PassThrough") + { + listen.UsePassThrough(); + } + }, + services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -956,21 +1151,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private static async Task AssertStreamContains(Stream stream, string expectedSubstring) { var expectedBytes = Encoding.ASCII.GetBytes(expectedSubstring); - var exptectedLength = expectedBytes.Length; - var responseBuffer = new byte[exptectedLength]; + var expectedLength = expectedBytes.Length; + var responseBuffer = new byte[expectedLength]; var matchedChars = 0; - while (matchedChars < exptectedLength) + while (matchedChars < expectedLength) { - var count = await stream.ReadAsync(responseBuffer, 0, exptectedLength - matchedChars).DefaultTimeout(); + var count = await stream.ReadAsync(responseBuffer, 0, expectedLength - matchedChars).DefaultTimeout(); if (count == 0) { Assert.True(false, "Stream completed without expected substring."); } - for (var i = 0; i < count && matchedChars < exptectedLength; i++) + for (var i = 0; i < count && matchedChars < expectedLength; i++) { if (responseBuffer[i] == expectedBytes[matchedChars]) { @@ -983,5 +1178,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } } + + private static void SetFinOnError(IServiceCollection services, bool finOnError) + { +#if LIBUV +#pragma warning disable CS0618 // Type or member is obsolete + services.Configure<LibuvTransportOptions>(options => + { + options.FinOnError = finOnError; + }); +#pragma warning restore CS0618 // Type or member is obsolete +#elif SOCKETS + services.Configure<SocketTransportOptions>(o => + { + o.FinOnError = finOnError; + }); +#endif + services.Configure<KestrelServerOptions>(o => + { + o.FinOnError = finOnError; + }); + } } } diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index 991044dbf8f05b7154b459f04f3349710357809b..cbda94bd05a61aa555a28494a4f518ff457b9178 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs @@ -23,7 +23,13 @@ using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.AspNetCore.Server.Kestrel.Tests; +#if LIBUV +using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv; +#elif SOCKETS +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +#endif using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -462,8 +468,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.Empty(coreLogs.Where(w => w.LogLevel > LogLevel.Information)); } - [Fact] - public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin) { var logger = LoggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); const int chunkSize = 1024; @@ -473,21 +481,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock<IKestrelTrace>(); - mockKestrelTrace - .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult()); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny<string>())) - .Callback(() => connectionStopMessageLogged.SetResult()); + TestSink.MessageLogged += context => + { + switch (context.EventId.Name) + { + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; + } + }; - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) @@ -533,7 +555,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - await using (var server = new TestServer(App, testContext)) + await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -553,8 +575,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Stream, chunkSize * chunks); + await AssertStreamAborted(connection.Stream, chunkSize * chunks, fin); sw.Stop(); logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); @@ -562,8 +592,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [Fact] - public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin) { const int chunkSize = 1024; const int chunks = 256 * 1024; @@ -573,21 +605,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock<IKestrelTrace>(); - mockKestrelTrace - .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult()); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny<string>())) - .Callback(() => connectionStopMessageLogged.SetResult()); + TestSink.MessageLogged += context => + { + switch (context.EventId.Name) + { + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; + } + }; - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) @@ -627,7 +673,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { await aborted.Task.DefaultTimeout(); } - }, testContext, ConfigureListenOptions)) + }, testContext, ConfigureListenOptions, + services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -642,16 +689,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await aborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Stream, chunkSize * chunks); + await AssertStreamAborted(connection.Stream, chunkSize * chunks, fin); } } } } - [Fact] - public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure(bool fin) { const int bufferSize = 65536; const int bufferCount = 100; @@ -660,21 +717,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock<IKestrelTrace>(); - mockKestrelTrace - .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny<string>(), It.IsAny<string>())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult()); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny<string>())) - .Callback(() => connectionStopMessageLogged.SetResult()); + TestSink.MessageLogged += context => + { + switch (context.EventId.Name) + { + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; + } + }; - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)), @@ -685,8 +756,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests testContext.InitializeHeartbeat(); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - async Task App(HttpContext context) { context.RequestAborted.Register(() => @@ -711,7 +780,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests copyToAsyncCts.SetException(new Exception("This shouldn't be reached.")); } - await using (var server = new TestServer(App, testContext, listenOptions)) + await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -736,10 +805,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout. await Assert.ThrowsAnyAsync<OperationCanceledException>(() => copyToAsyncCts.Task).DefaultTimeout(); - await AssertStreamAborted(connection.Stream, responseSize); + await AssertStreamAborted(connection.Stream, responseSize, graceful: false); } } } @@ -985,7 +1062,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.False(requestAborted); } - private async Task AssertStreamAborted(Stream stream, int totalBytes) + private async Task AssertStreamAborted(Stream stream, int totalBytes, bool graceful) { var receiveBuffer = new byte[64 * 1024]; var totalReceived = 0; @@ -998,6 +1075,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests if (bytes == 0) { + Assert.True(graceful, "Stream completed gracefully."); + break; } @@ -1006,7 +1085,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } catch (IOException) { - // This is expected given an abort. + Assert.False(graceful, "Stream completed abortively."); } Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully."); @@ -1080,5 +1159,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests return dataset; } } + + private static void SetFinOnError(IServiceCollection services, bool finOnError) + { +#if LIBUV +#pragma warning disable CS0618 // Type or member is obsolete + services.Configure<LibuvTransportOptions>(options => + { + options.FinOnError = finOnError; + }); +#pragma warning restore CS0618 // Type or member is obsolete +#elif SOCKETS + services.Configure<SocketTransportOptions>(o => + { + o.FinOnError = finOnError; + }); +#endif + services.Configure<KestrelServerOptions>(o => + { + o.FinOnError = finOnError; + }); + } } }