Skip to content

Commit 5a4c82e

Browse files
amcaseyadityamandaleeka
authored andcommitted
Merged PR 32090: [7.0] Forcibly close socket on abort
Forcibly close socket when connection is aborted.
1 parent 5b4c366 commit 5a4c82e

File tree

15 files changed

+443
-64
lines changed

15 files changed

+443
-64
lines changed

src/Servers/IIS/IIS/test/Common.LongTests/ShutdownTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,8 +535,15 @@ public async Task ClosesConnectionOnServerAbortOutOfProcess()
535535
var response = await deploymentResult.HttpClient.GetAsync("/Abort").TimeoutAfter(TimeoutExtensions.DefaultTimeoutValue);
536536

537537
Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode);
538+
539+
#if NEWSHIM_FUNCTIONALS
540+
// In-proc SocketConnection isn't used and there's no abort
538541
// 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response
539542
Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync());
543+
#else
544+
// 0x80072efe ERROR_INTERNET_CONNECTION_ABORTED The connection with the server was terminated abnormally
545+
Assert.Contains("0x80072efe", await response.Content.ReadAsStringAsync());
546+
#endif
540547
}
541548
catch (HttpRequestException)
542549
{

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,14 @@ protected override void OnRequestProcessingEnded()
9898
_http1Output.Dispose();
9999
}
100100

101-
public void OnInputOrOutputCompleted()
101+
void IRequestProcessor.OnInputOrOutputCompleted()
102+
{
103+
// Closed gracefully.
104+
_http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
105+
CancelRequestAbortedToken();
106+
}
107+
108+
void IHttpOutputAborter.OnInputOrOutputCompleted()
102109
{
103110
_http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
104111
CancelRequestAbortedToken();

src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ protected void ThrowUnexpectedEndOfRequestContent()
204204
// so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400
205205
// response is written after observing the unexpected end of request content instead of just
206206
// closing the connection without a response as expected.
207-
_context.OnInputOrOutputCompleted();
207+
((IHttpOutputAborter)_context).OnInputOrOutputCompleted();
208208

209209
KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
210210
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ public Http2Connection(HttpConnectionContext context)
159159
public void OnInputOrOutputCompleted()
160160
{
161161
TryClose();
162-
_frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
162+
var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0;
163+
_frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
163164
}
164165

165166
public void Abort(ConnectionAbortedException ex)

src/Servers/Kestrel/Core/src/KestrelServerOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core;
2626
public class KestrelServerOptions
2727
{
2828
internal const string DisableHttp1LineFeedTerminatorsSwitchKey = "Microsoft.AspNetCore.Server.Kestrel.DisableHttp1LineFeedTerminators";
29+
private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
30+
private static readonly bool _finOnError;
31+
32+
static KestrelServerOptions()
33+
{
34+
AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
35+
}
2936

3037
// internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged.
3138
internal static readonly Func<string, Encoding?> DefaultHeaderEncodingSelector = _ => null;
3239

40+
// Opt-out flag for back compat. Remove in 9.0 (or make public).
41+
internal bool FinOnError { get; set; } = _finOnError;
42+
3343
private Func<string, Encoding?> _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector;
3444

3545
private Func<string, Encoding?> _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector;

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ internal sealed partial class SocketConnection : TransportConnection
3030
private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource();
3131
private bool _connectionClosed;
3232
private readonly bool _waitForData;
33+
private readonly bool _finOnError;
3334

3435
internal SocketConnection(Socket socket,
3536
MemoryPool<byte> memoryPool,
@@ -38,7 +39,8 @@ internal SocketConnection(Socket socket,
3839
SocketSenderPool socketSenderPool,
3940
PipeOptions inputOptions,
4041
PipeOptions outputOptions,
41-
bool waitForData = true)
42+
bool waitForData = true,
43+
bool finOnError = false)
4244
{
4345
Debug.Assert(socket != null);
4446
Debug.Assert(memoryPool != null);
@@ -49,6 +51,7 @@ internal SocketConnection(Socket socket,
4951
_logger = logger;
5052
_waitForData = waitForData;
5153
_socketSenderPool = socketSenderPool;
54+
_finOnError = finOnError;
5255

5356
LocalEndPoint = _socket.LocalEndPoint;
5457
RemoteEndPoint = _socket.RemoteEndPoint;
@@ -380,11 +383,21 @@ private void Shutdown(Exception? shutdownReason)
380383
// ever observe the nondescript ConnectionAbortedException except for connection middleware attempting
381384
// to half close the connection which is currently unsupported.
382385
_shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully.");
386+
387+
// NB: not _shutdownReason since we don't want to do this on graceful completion
388+
if (!_finOnError && shutdownReason is not null)
389+
{
390+
SocketsLog.ConnectionWriteRst(_logger, this, shutdownReason.Message);
391+
392+
// This forces an abortive close with linger time 0 (and implies Dispose)
393+
_socket.Close(timeout: 0);
394+
return;
395+
}
396+
383397
SocketsLog.ConnectionWriteFin(_logger, this, _shutdownReason.Message);
384398

385399
try
386400
{
387-
// Try to gracefully close the socket even for aborts to match libuv behavior.
388401
_socket.Shutdown(SocketShutdown.Both);
389402
}
390403
catch

src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsLog.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ public static void ConnectionWriteFin(ILogger logger, SocketConnection connectio
3131
}
3232
}
3333

34+
[LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = "ConnectionWriteRst", SkipEnabledCheck = true)]
35+
private static partial void ConnectionWriteRstCore(ILogger logger, string connectionId, string reason);
36+
37+
public static void ConnectionWriteRst(ILogger logger, SocketConnection connection, string reason)
38+
{
39+
if (logger.IsEnabled(LogLevel.Debug))
40+
{
41+
ConnectionWriteRstCore(logger, connection.ConnectionId, reason);
42+
}
43+
}
44+
3445
// Reserved: Event ID 11, EventName = ConnectionWrite
3546

3647
// Reserved: Event ID 12, EventName = ConnectionWriteCallback

src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ public ConnectionContext Create(Socket socket)
113113
setting.SocketSenderPool,
114114
setting.InputOptions,
115115
setting.OutputOptions,
116-
waitForData: _options.WaitForDataBeforeAllocatingBuffer);
116+
waitForData: _options.WaitForDataBeforeAllocatingBuffer,
117+
finOnError: _options.FinOnError);
117118

118119
connection.Start();
119120
return connection;

src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions)
2323
MaxWriteBufferSize = transportOptions.MaxWriteBufferSize;
2424
UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling;
2525
MemoryPoolFactory = transportOptions.MemoryPoolFactory;
26+
FinOnError = transportOptions.FinOnError;
2627
}
2728

29+
// Opt-out flag for back compat. Remove in 9.0 (or make public).
30+
internal bool FinOnError { get; set; }
31+
2832
/// <summary>
2933
/// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool.
3034
/// </summary>

src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets;
1313
/// </summary>
1414
public class SocketTransportOptions
1515
{
16+
private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError";
17+
private static readonly bool _finOnError;
18+
19+
static SocketTransportOptions()
20+
{
21+
AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError);
22+
}
23+
24+
// Opt-out flag for back compat. Remove in 9.0 (or make public).
25+
internal bool FinOnError { get; set; } = _finOnError;
26+
1627
/// <summary>
1728
/// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool.
1829
/// </summary>

src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public async Task ReceiveEnd(params string[] lines)
138138
public async Task WaitForConnectionClose()
139139
{
140140
var buffer = new byte[128];
141-
var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout);
141+
var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).ContinueWith(t => t.IsFaulted ? 0 : t.Result).TimeoutAfter(Timeout);
142142

143143
if (bytesTransferred > 0)
144144
{

src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions
4848
{
4949
}
5050

51-
public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions)
51+
public TestServer(RequestDelegate app, TestServiceContext context, Action<ListenOptions> configureListenOptions, Action<IServiceCollection> configureServices = null)
5252
: this(app, context, options =>
5353
{
5454
var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
@@ -57,7 +57,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action<Listen
5757
};
5858
configureListenOptions(listenOptions);
5959
options.CodeBackedListenOptions.Add(listenOptions);
60-
}, _ => { })
60+
}, s =>
61+
{
62+
configureServices?.Invoke(s);
63+
})
6164
{
6265
}
6366

src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,60 @@ public ShutdownTests()
4444
};
4545
}
4646

47+
[ConditionalFact]
48+
public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN()
49+
{
50+
var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
51+
var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
52+
var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
53+
54+
TestSink.MessageLogged += context =>
55+
{
56+
57+
if (context.EventId.Name == "Http2ConnectionClosed")
58+
{
59+
connectionClosed.SetResult();
60+
}
61+
else if (context.EventId.Name == "ConnectionReadFin")
62+
{
63+
readFin.SetResult();
64+
}
65+
else if (context.EventId.Name == "ConnectionWriteFin")
66+
{
67+
writeFin.SetResult();
68+
}
69+
};
70+
71+
var testContext = new TestServiceContext(LoggerFactory);
72+
73+
testContext.InitializeHeartbeat();
74+
75+
await using (var server = new TestServer(context =>
76+
{
77+
return context.Response.WriteAsync("hello world " + context.Request.Protocol);
78+
},
79+
testContext,
80+
kestrelOptions =>
81+
{
82+
kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions =>
83+
{
84+
listenOptions.Protocols = HttpProtocols.Http2;
85+
listenOptions.UseHttps(_x509Certificate2);
86+
});
87+
}))
88+
{
89+
var response = await Client.GetStringAsync($"https://localhost:{server.Port}/");
90+
Assert.Equal("hello world HTTP/2", response);
91+
Client.Dispose(); // Close the socket, no GoAway is sent.
92+
93+
await readFin.Task.DefaultTimeout();
94+
await writeFin.Task.DefaultTimeout();
95+
await connectionClosed.Task.DefaultTimeout();
96+
97+
await server.StopAsync();
98+
}
99+
}
100+
47101
[CollectDump]
48102
[ConditionalFact]
49103
public async Task GracefulShutdownWaitsForRequestsToFinish()

0 commit comments

Comments
 (0)