255 lines
9.1 KiB
C#
255 lines
9.1 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
#if !INNER_LOOP
|
|
using System;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
|
using Microsoft.AspNetCore.Testing;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|
{
|
|
public class KeepAliveTimeoutTests
|
|
{
|
|
private static readonly TimeSpan _keepAliveTimeout = TimeSpan.FromSeconds(10);
|
|
private static readonly TimeSpan _longDelay = TimeSpan.FromSeconds(30);
|
|
private static readonly TimeSpan _shortDelay = TimeSpan.FromSeconds(_longDelay.TotalSeconds / 10);
|
|
|
|
[Fact]
|
|
public Task TestKeepAliveTimeout()
|
|
{
|
|
// Delays in these tests cannot be much longer than expected.
|
|
// Call Task.Run() to get rid of Xunit's synchronization context,
|
|
// otherwise it can cause unexpectedly longer delays when multiple tests
|
|
// are running in parallel. These tests becomes flaky on slower
|
|
// hardware because the continuations for the delay tasks might take too long to be
|
|
// scheduled if running on Xunit's synchronization context.
|
|
return Task.Run(async () =>
|
|
{
|
|
var longRunningCancellationTokenSource = new CancellationTokenSource();
|
|
var upgradeCancellationTokenSource = new CancellationTokenSource();
|
|
|
|
using (var server = CreateServer(longRunningCancellationTokenSource.Token, upgradeCancellationTokenSource.Token))
|
|
{
|
|
var tasks = new[]
|
|
{
|
|
ConnectionClosedWhenKeepAliveTimeoutExpires(server),
|
|
ConnectionKeptAliveBetweenRequests(server),
|
|
ConnectionNotTimedOutWhileRequestBeingSent(server),
|
|
ConnectionNotTimedOutWhileAppIsRunning(server, longRunningCancellationTokenSource),
|
|
ConnectionTimesOutWhenOpenedButNoRequestSent(server),
|
|
KeepAliveTimeoutDoesNotApplyToUpgradedConnections(server, upgradeCancellationTokenSource)
|
|
};
|
|
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task ConnectionClosedWhenKeepAliveTimeoutExpires(TestServer server)
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"Host:",
|
|
"",
|
|
"");
|
|
await ReceiveResponse(connection);
|
|
await connection.WaitForConnectionClose().TimeoutAfter(_longDelay);
|
|
}
|
|
}
|
|
|
|
private async Task ConnectionKeptAliveBetweenRequests(TestServer server)
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"Host:",
|
|
"",
|
|
"");
|
|
|
|
// Don't change this to Task.Delay. See https://github.com/aspnet/KestrelHttpServer/issues/1684#issuecomment-330285740.
|
|
Thread.Sleep(_shortDelay);
|
|
}
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
await ReceiveResponse(connection);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task ConnectionNotTimedOutWhileRequestBeingSent(TestServer server)
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
var cts = new CancellationTokenSource();
|
|
cts.CancelAfter(_longDelay);
|
|
|
|
await connection.Send(
|
|
"POST /consume HTTP/1.1",
|
|
"Host:",
|
|
"Transfer-Encoding: chunked",
|
|
"",
|
|
"");
|
|
|
|
while (!cts.IsCancellationRequested)
|
|
{
|
|
await connection.Send(
|
|
"1",
|
|
"a",
|
|
"");
|
|
await Task.Delay(_shortDelay);
|
|
}
|
|
|
|
await connection.Send(
|
|
"0",
|
|
"",
|
|
"");
|
|
await ReceiveResponse(connection);
|
|
}
|
|
}
|
|
|
|
private async Task ConnectionNotTimedOutWhileAppIsRunning(TestServer server, CancellationTokenSource cts)
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET /longrunning HTTP/1.1",
|
|
"Host:",
|
|
"",
|
|
"");
|
|
cts.CancelAfter(_longDelay);
|
|
|
|
while (!cts.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(1000);
|
|
}
|
|
|
|
await ReceiveResponse(connection);
|
|
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"Host:",
|
|
"",
|
|
"");
|
|
await ReceiveResponse(connection);
|
|
}
|
|
}
|
|
|
|
private async Task ConnectionTimesOutWhenOpenedButNoRequestSent(TestServer server)
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await Task.Delay(_longDelay);
|
|
await connection.WaitForConnectionClose().TimeoutAfter(_longDelay);
|
|
}
|
|
}
|
|
|
|
private async Task KeepAliveTimeoutDoesNotApplyToUpgradedConnections(TestServer server, CancellationTokenSource cts)
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET /upgrade HTTP/1.1",
|
|
"Host:",
|
|
"Connection: Upgrade",
|
|
"",
|
|
"");
|
|
await connection.Receive(
|
|
"HTTP/1.1 101 Switching Protocols",
|
|
"Connection: Upgrade",
|
|
"");
|
|
await connection.ReceiveStartsWith("Date: ");
|
|
await connection.Receive(
|
|
"",
|
|
"");
|
|
cts.CancelAfter(_longDelay);
|
|
|
|
while (!cts.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(1000);
|
|
}
|
|
|
|
await connection.Receive("hello, world");
|
|
}
|
|
}
|
|
|
|
private TestServer CreateServer(CancellationToken longRunningCt, CancellationToken upgradeCt)
|
|
{
|
|
return new TestServer(httpContext => App(httpContext, longRunningCt, upgradeCt), new TestServiceContext
|
|
{
|
|
// Use real SystemClock so timeouts trigger.
|
|
SystemClock = new SystemClock(),
|
|
ServerOptions =
|
|
{
|
|
AddServerHeader = false,
|
|
Limits =
|
|
{
|
|
KeepAliveTimeout = _keepAliveTimeout,
|
|
MinRequestBodyDataRate = null
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task App(HttpContext httpContext, CancellationToken longRunningCt, CancellationToken upgradeCt)
|
|
{
|
|
var ct = httpContext.RequestAborted;
|
|
var responseStream = httpContext.Response.Body;
|
|
var responseBytes = Encoding.ASCII.GetBytes("hello, world");
|
|
|
|
if (httpContext.Request.Path == "/longrunning")
|
|
{
|
|
while (!longRunningCt.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(1000);
|
|
}
|
|
}
|
|
else if (httpContext.Request.Path == "/upgrade")
|
|
{
|
|
using (var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync())
|
|
{
|
|
while (!upgradeCt.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(_longDelay);
|
|
}
|
|
|
|
responseStream = stream;
|
|
}
|
|
}
|
|
else if (httpContext.Request.Path == "/consume")
|
|
{
|
|
var buffer = new byte[1024];
|
|
while (await httpContext.Request.Body.ReadAsync(buffer, 0, buffer.Length) > 0) ;
|
|
}
|
|
|
|
await responseStream.WriteAsync(responseBytes, 0, responseBytes.Length);
|
|
}
|
|
|
|
private async Task ReceiveResponse(TestConnection connection)
|
|
{
|
|
await connection.Receive(
|
|
"HTTP/1.1 200 OK",
|
|
"");
|
|
await connection.ReceiveStartsWith("Date: ");
|
|
await connection.Receive(
|
|
"Transfer-Encoding: chunked",
|
|
"",
|
|
"c",
|
|
"hello, world",
|
|
"0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
#endif |