1431 lines
54 KiB
C#
1431 lines
54 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.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.Server.Kestrel;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Internal;
|
|
using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
|
|
using Microsoft.AspNetCore.Testing;
|
|
using Microsoft.Extensions.Internal;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Server.KestrelTests
|
|
{
|
|
/// <summary>
|
|
/// Summary description for EngineTests
|
|
/// </summary>
|
|
public class EngineTests
|
|
{
|
|
public static TheoryData<ListenOptions> ConnectionAdapterData => new TheoryData<ListenOptions>
|
|
{
|
|
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)),
|
|
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0))
|
|
{
|
|
ConnectionAdapters = { new PassThroughConnectionAdapter() }
|
|
}
|
|
};
|
|
|
|
[Fact]
|
|
public async Task EngineCanStartAndStop()
|
|
{
|
|
var serviceContext = new TestServiceContext();
|
|
|
|
// The engine can no longer start threads without binding to an endpoint.
|
|
var engine = new KestrelEngine(serviceContext.TransportContext,
|
|
new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)));
|
|
|
|
await engine.BindAsync();
|
|
await engine.StopAsync();
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ListenerCanCreateAndDispose(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
testContext.App = TestApp.EchoApp;
|
|
var engine = new KestrelEngine(testContext.TransportContext, listenOptions);
|
|
|
|
await engine.BindAsync();
|
|
await engine.UnbindAsync();
|
|
await engine.StopAsync();
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ConnectionCanReadAndWrite(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
testContext.App = TestApp.EchoApp;
|
|
var engine = new KestrelEngine(testContext.TransportContext, listenOptions);
|
|
|
|
await engine.BindAsync();
|
|
|
|
var socket = TestConnection.CreateConnectedLoopbackSocket(listenOptions.IPEndPoint.Port);
|
|
var data = "Hello World";
|
|
socket.Send(Encoding.ASCII.GetBytes($"POST / HTTP/1.0\r\nContent-Length: 11\r\n\r\n{data}"));
|
|
var buffer = new byte[data.Length];
|
|
var read = 0;
|
|
while (read < data.Length)
|
|
{
|
|
read += socket.Receive(buffer, read, buffer.Length - read, SocketFlags.None);
|
|
}
|
|
socket.Dispose();
|
|
|
|
await engine.UnbindAsync();
|
|
await engine.StopAsync();
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Http10RequestReceivesHttp11Response(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.0",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"Hello World");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Http11(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"GET / HTTP/1.1",
|
|
"Connection: close",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task HeadersAndStreamsAreReused(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
var streamCount = 0;
|
|
var requestHeadersCount = 0;
|
|
var responseHeadersCount = 0;
|
|
var loopCount = 20;
|
|
Stream lastStream = null;
|
|
IHeaderDictionary lastRequestHeaders = null;
|
|
IHeaderDictionary lastResponseHeaders = null;
|
|
|
|
using (var server = new TestServer(
|
|
async context =>
|
|
{
|
|
if (context.Request.Body != lastStream)
|
|
{
|
|
lastStream = context.Request.Body;
|
|
streamCount++;
|
|
}
|
|
if (context.Request.Headers != lastRequestHeaders)
|
|
{
|
|
lastRequestHeaders = context.Request.Headers;
|
|
requestHeadersCount++;
|
|
}
|
|
if (context.Response.Headers != lastResponseHeaders)
|
|
{
|
|
lastResponseHeaders = context.Response.Headers;
|
|
responseHeadersCount++;
|
|
}
|
|
|
|
var ms = new MemoryStream();
|
|
await context.Request.Body.CopyToAsync(ms);
|
|
var request = ms.ToArray();
|
|
|
|
context.Response.ContentLength = request.Length;
|
|
|
|
await context.Response.Body.WriteAsync(request, 0, request.Length);
|
|
},
|
|
testContext))
|
|
{
|
|
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
var requestData =
|
|
Enumerable.Repeat("GET / HTTP/1.1\r\n", loopCount)
|
|
.Concat(new[] { "GET / HTTP/1.1\r\nContent-Length: 7\r\nConnection: close\r\n\r\nGoodbye" });
|
|
|
|
var response = string.Join("\r\n", new string[] {
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
""});
|
|
|
|
var lastResponse = string.Join("\r\n", new string[]
|
|
{
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye"
|
|
});
|
|
|
|
var responseData =
|
|
Enumerable.Repeat(response, loopCount)
|
|
.Concat(new[] { lastResponse });
|
|
|
|
await connection.Send(requestData.ToArray());
|
|
|
|
await connection.ReceiveEnd(responseData.ToArray());
|
|
}
|
|
|
|
Assert.Equal(1, streamCount);
|
|
Assert.Equal(1, requestHeadersCount);
|
|
Assert.Equal(1, responseHeadersCount);
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Http10ContentLength(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.0",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"Hello World");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Http10KeepAlive(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"Connection: keep-alive",
|
|
"",
|
|
"POST / HTTP/1.0",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
await connection.Receive(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: keep-alive",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"\r\n");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Http10KeepAliveNotUsedIfResponseContentLengthNotSet(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"Connection: keep-alive",
|
|
"",
|
|
"POST / HTTP/1.0",
|
|
"Connection: keep-alive",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
await connection.Receive(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: keep-alive",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"\r\n");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"Goodbye");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Http10KeepAliveContentLength(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.0",
|
|
"Content-Length: 11",
|
|
"Connection: keep-alive",
|
|
"",
|
|
"Hello WorldPOST / HTTP/1.0",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
await connection.Receive(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: keep-alive",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 7",
|
|
"",
|
|
"Goodbye");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task Expect100ContinueForBody(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.1",
|
|
"Expect: 100-continue",
|
|
"Connection: close",
|
|
"Content-Length: 11",
|
|
"\r\n");
|
|
await connection.Receive(
|
|
"HTTP/1.1 100 Continue",
|
|
"",
|
|
"");
|
|
await connection.Send("Hello World");
|
|
await connection.Receive(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task DisconnectingClient(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoApp, testContext, listenOptions))
|
|
{
|
|
var socket = TestConnection.CreateConnectedLoopbackSocket(server.Port);
|
|
await Task.Delay(200);
|
|
socket.Dispose();
|
|
|
|
await Task.Delay(200);
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ZeroContentLengthSetAutomaticallyAfterNoWrites(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EmptyApp, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"GET / HTTP/1.0",
|
|
"Connection: keep-alive",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: keep-alive",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ZeroContentLengthSetAutomaticallyForNonKeepAliveRequests(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).TimeoutAfter(TimeSpan.FromSeconds(10)));
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"Connection: close",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ZeroContentLengthNotSetAutomaticallyForHeadRequests(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EmptyApp, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"HEAD / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var request = httpContext.Request;
|
|
var response = httpContext.Response;
|
|
|
|
using (var reader = new StreamReader(request.Body, Encoding.ASCII))
|
|
{
|
|
var statusString = await reader.ReadLineAsync();
|
|
response.StatusCode = int.Parse(statusString);
|
|
}
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.1",
|
|
"Content-Length: 3",
|
|
"",
|
|
"204POST / HTTP/1.1",
|
|
"Content-Length: 3",
|
|
"",
|
|
"205POST / HTTP/1.1",
|
|
"Content-Length: 3",
|
|
"",
|
|
"304POST / HTTP/1.1",
|
|
"Content-Length: 3",
|
|
"",
|
|
"200");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 204 No Content",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"HTTP/1.1 205 Reset Content",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"HTTP/1.1 304 Not Modified",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ZeroContentLengthAssumedOnNonKeepAliveRequestsWithoutContentLengthOrTransferEncodingHeader(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
// This will hang if 0 content length is not assumed by the server
|
|
Assert.Equal(0, await httpContext.Request.Body.ReadAsync(new byte[1], 0, 1).TimeoutAfter(TimeSpan.FromSeconds(10)));
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
// Use Send instead of SendEnd to ensure the connection will remain open while
|
|
// the app runs and reads 0 bytes from the body nonetheless. This checks that
|
|
// https://github.com/aspnet/KestrelHttpServer/issues/1104 is not regressing.
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"Connection: close",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ConnectionClosedAfter101Response(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var request = httpContext.Request;
|
|
var stream = await httpContext.Features.Get<IHttpUpgradeFeature>().UpgradeAsync();
|
|
var response = Encoding.ASCII.GetBytes("hello, world");
|
|
await stream.WriteAsync(response, 0, response.Length);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 101 Switching Protocols",
|
|
"Connection: Upgrade",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"hello, world");
|
|
}
|
|
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"Connection: keep-alive",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 101 Switching Protocols",
|
|
"Connection: Upgrade",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
"hello, world");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ThrowingResultsIn500Response(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
bool onStartingCalled = false;
|
|
|
|
var testLogger = new TestApplicationErrorLogger();
|
|
testContext.Log = new KestrelTrace(testLogger);
|
|
|
|
using (var server = new TestServer(httpContext =>
|
|
{
|
|
var response = httpContext.Response;
|
|
response.OnStarting(_ =>
|
|
{
|
|
onStartingCalled = true;
|
|
return TaskCache.CompletedTask;
|
|
}, null);
|
|
|
|
// Anything added to the ResponseHeaders dictionary is ignored
|
|
response.Headers["Content-Length"] = "11";
|
|
throw new Exception();
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"GET / HTTP/1.1",
|
|
"Connection: close",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 500 Internal Server Error",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 500 Internal Server Error",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
|
|
Assert.False(onStartingCalled);
|
|
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ThrowingAfterWritingKillsConnection(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
bool onStartingCalled = false;
|
|
|
|
var testLogger = new TestApplicationErrorLogger();
|
|
testContext.Log = new KestrelTrace(testLogger);
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var response = httpContext.Response;
|
|
response.OnStarting(_ =>
|
|
{
|
|
onStartingCalled = true;
|
|
return Task.FromResult<object>(null);
|
|
}, null);
|
|
|
|
response.Headers["Content-Length"] = new[] { "11" };
|
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
|
throw new Exception();
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
}
|
|
}
|
|
|
|
Assert.True(onStartingCalled);
|
|
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ThrowingAfterPartialWriteKillsConnection(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
bool onStartingCalled = false;
|
|
|
|
var testLogger = new TestApplicationErrorLogger();
|
|
testContext.Log = new KestrelTrace(testLogger);
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var response = httpContext.Response;
|
|
response.OnStarting(_ =>
|
|
{
|
|
onStartingCalled = true;
|
|
return Task.FromResult<object>(null);
|
|
}, null);
|
|
|
|
response.Headers["Content-Length"] = new[] { "11" };
|
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello"), 0, 5);
|
|
throw new Exception();
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello");
|
|
}
|
|
}
|
|
|
|
Assert.True(onStartingCalled);
|
|
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
|
|
}
|
|
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ConnectionClosesWhenFinReceivedBeforeRequestCompletes(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(TestApp.EchoAppChunked, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"POST / HTTP/1.1");
|
|
connection.Shutdown(SocketShutdown.Send);
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 400 Bad Request",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"POST / HTTP/1.1",
|
|
"Content-Length: 7");
|
|
connection.Shutdown(SocketShutdown.Send);
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 400 Bad Request",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ThrowingInOnStartingResultsInFailedWritesAnd500Response(ListenOptions listenOptions)
|
|
{
|
|
var callback1Called = false;
|
|
var callback2CallCount = 0;
|
|
|
|
var testContext = new TestServiceContext();
|
|
var testLogger = new TestApplicationErrorLogger();
|
|
testContext.Log = new KestrelTrace(testLogger);
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var onStartingException = new Exception();
|
|
|
|
var response = httpContext.Response;
|
|
response.OnStarting(_ =>
|
|
{
|
|
callback1Called = true;
|
|
throw onStartingException;
|
|
}, null);
|
|
response.OnStarting(_ =>
|
|
{
|
|
callback2CallCount++;
|
|
throw onStartingException;
|
|
}, null);
|
|
|
|
var writeException = await Assert.ThrowsAsync<ObjectDisposedException>(async () => await response.Body.FlushAsync());
|
|
Assert.Same(onStartingException, writeException.InnerException);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 500 Internal Server Error",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 500 Internal Server Error",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
|
|
// The first registered OnStarting callback should have been called,
|
|
// since they are called LIFO order and the other one failed.
|
|
Assert.False(callback1Called);
|
|
Assert.Equal(2, callback2CallCount);
|
|
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ThrowingInOnCompletedIsLoggedAndClosesConnection(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
var onCompletedCalled1 = false;
|
|
var onCompletedCalled2 = false;
|
|
|
|
var testLogger = new TestApplicationErrorLogger();
|
|
testContext.Log = new KestrelTrace(testLogger);
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var response = httpContext.Response;
|
|
response.OnCompleted(_ =>
|
|
{
|
|
onCompletedCalled1 = true;
|
|
throw new Exception();
|
|
}, null);
|
|
response.OnCompleted(_ =>
|
|
{
|
|
onCompletedCalled2 = true;
|
|
throw new Exception();
|
|
}, null);
|
|
|
|
response.Headers["Content-Length"] = new[] { "11" };
|
|
|
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
}
|
|
}
|
|
|
|
// All OnCompleted callbacks should be called even if they throw.
|
|
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
|
|
Assert.True(onCompletedCalled1);
|
|
Assert.True(onCompletedCalled2);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task RequestsCanBeAbortedMidRead(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
var readTcs = new TaskCompletionSource<object>();
|
|
var registrationTcs = new TaskCompletionSource<int>();
|
|
var requestId = 0;
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
requestId++;
|
|
|
|
var response = httpContext.Response;
|
|
var request = httpContext.Request;
|
|
var lifetime = httpContext.Features.Get<IHttpRequestLifetimeFeature>();
|
|
|
|
lifetime.RequestAborted.Register(() => registrationTcs.TrySetResult(requestId));
|
|
|
|
if (requestId == 1)
|
|
{
|
|
response.Headers["Content-Length"] = new[] { "5" };
|
|
|
|
await response.WriteAsync("World");
|
|
}
|
|
else
|
|
{
|
|
var readTask = request.Body.CopyToAsync(Stream.Null);
|
|
|
|
lifetime.Abort();
|
|
|
|
try
|
|
{
|
|
await readTask;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
readTcs.SetException(ex);
|
|
throw;
|
|
}
|
|
|
|
readTcs.SetException(new Exception("This shouldn't be reached."));
|
|
}
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
// Never send the body so CopyToAsync always fails.
|
|
await connection.Send(
|
|
"POST / HTTP/1.1",
|
|
"Content-Length: 5",
|
|
"",
|
|
"HelloPOST / HTTP/1.1",
|
|
"Content-Length: 5",
|
|
"",
|
|
"");
|
|
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 5",
|
|
"",
|
|
"World");
|
|
}
|
|
}
|
|
|
|
await Assert.ThrowsAsync<TaskCanceledException>(async () => await readTcs.Task);
|
|
|
|
// The cancellation token for only the last request should be triggered.
|
|
var abortedRequestId = await registrationTcs.Task;
|
|
Assert.Equal(2, abortedRequestId);
|
|
}
|
|
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task FailedWritesResultInAbortedRequest(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
// This should match _maxBytesPreCompleted in SocketOutput
|
|
var maxBytesPreCompleted = 65536;
|
|
// Ensure string is long enough to disable write-behind buffering
|
|
var largeString = new string('a', maxBytesPreCompleted + 1);
|
|
|
|
var writeTcs = new TaskCompletionSource<object>();
|
|
var registrationWh = new ManualResetEventSlim();
|
|
var connectionCloseWh = new ManualResetEventSlim();
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var response = httpContext.Response;
|
|
var request = httpContext.Request;
|
|
var lifetime = httpContext.Features.Get<IHttpRequestLifetimeFeature>();
|
|
|
|
lifetime.RequestAborted.Register(() => registrationWh.Set());
|
|
|
|
await request.Body.CopyToAsync(Stream.Null);
|
|
connectionCloseWh.Wait();
|
|
|
|
try
|
|
{
|
|
// Ensure write is long enough to disable write-behind buffering
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
await response.WriteAsync(largeString, lifetime.RequestAborted);
|
|
registrationWh.Wait(1000);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
writeTcs.SetException(ex);
|
|
throw;
|
|
}
|
|
|
|
writeTcs.SetException(new Exception("This shouldn't be reached."));
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.1",
|
|
"Content-Length: 5",
|
|
"",
|
|
"Hello");
|
|
// Don't wait to receive the response. Just close the socket.
|
|
}
|
|
|
|
connectionCloseWh.Set();
|
|
|
|
// Write failed
|
|
await Assert.ThrowsAsync<TaskCanceledException>(async () => await writeTcs.Task);
|
|
// RequestAborted tripped
|
|
Assert.True(registrationWh.Wait(1000));
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task NoErrorsLoggedWhenServerEndsConnectionBeforeClient(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
var testLogger = new TestApplicationErrorLogger();
|
|
testContext.Log = new KestrelTrace(testLogger);
|
|
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var response = httpContext.Response;
|
|
response.Headers["Content-Length"] = new[] { "11" };
|
|
await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.0",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 200 OK",
|
|
"Connection: close",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 11",
|
|
"",
|
|
"Hello World");
|
|
}
|
|
}
|
|
|
|
Assert.Equal(0, testLogger.TotalErrorsLogged);
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task NoResponseSentWhenConnectionIsClosedByServerBeforeClientFinishesSendingRequest(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(httpContext =>
|
|
{
|
|
httpContext.Abort();
|
|
return TaskCache.CompletedTask;
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"POST / HTTP/1.0",
|
|
"Content-Length: 1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveForcedEnd();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task RequestHeadersAreResetOnEachRequest(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
IHeaderDictionary originalRequestHeaders = null;
|
|
var firstRequest = true;
|
|
|
|
using (var server = new TestServer(httpContext =>
|
|
{
|
|
var requestFeature = httpContext.Features.Get<IHttpRequestFeature>();
|
|
|
|
if (firstRequest)
|
|
{
|
|
originalRequestHeaders = requestFeature.Headers;
|
|
requestFeature.Headers = new FrameRequestHeaders();
|
|
firstRequest = false;
|
|
}
|
|
else
|
|
{
|
|
Assert.Same(originalRequestHeaders, requestFeature.Headers);
|
|
}
|
|
|
|
return TaskCache.CompletedTask;
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task ResponseHeadersAreResetOnEachRequest(ListenOptions listenOptions)
|
|
{
|
|
var testContext = new TestServiceContext();
|
|
|
|
IHeaderDictionary originalResponseHeaders = null;
|
|
var firstRequest = true;
|
|
|
|
using (var server = new TestServer(httpContext =>
|
|
{
|
|
var responseFeature = httpContext.Features.Get<IHttpResponseFeature>();
|
|
|
|
if (firstRequest)
|
|
{
|
|
originalResponseHeaders = responseFeature.Headers;
|
|
responseFeature.Headers = new FrameResponseHeaders();
|
|
firstRequest = false;
|
|
}
|
|
else
|
|
{
|
|
Assert.Same(originalResponseHeaders, responseFeature.Headers);
|
|
}
|
|
|
|
return TaskCache.CompletedTask;
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"Content-Length: 0",
|
|
"",
|
|
"");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/%%2000", "/% 00")]
|
|
[InlineData("/%25%30%30", "/%00")]
|
|
public async Task PathEscapeTests(string inputPath, string expectedPath)
|
|
{
|
|
using (var server = new TestServer(async httpContext =>
|
|
{
|
|
var path = httpContext.Request.Path.Value;
|
|
httpContext.Response.Headers["Content-Length"] = new[] { path.Length.ToString() };
|
|
await httpContext.Response.WriteAsync(path);
|
|
}))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
$"GET {inputPath} HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {server.Context.DateHeaderValue}",
|
|
$"Content-Length: {expectedPath.Length.ToString()}",
|
|
"",
|
|
$"{expectedPath}");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task OnStartingCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions)
|
|
{
|
|
const string response = "hello, world";
|
|
|
|
var testContext = new TestServiceContext();
|
|
|
|
var callOrder = new Stack<int>();
|
|
var onStartingTcs = new TaskCompletionSource<object>();
|
|
|
|
using (var server = new TestServer(async context =>
|
|
{
|
|
context.Response.OnStarting(_ =>
|
|
{
|
|
callOrder.Push(1);
|
|
onStartingTcs.SetResult(null);
|
|
return TaskCache.CompletedTask;
|
|
}, null);
|
|
context.Response.OnStarting(_ =>
|
|
{
|
|
callOrder.Push(2);
|
|
return TaskCache.CompletedTask;
|
|
}, null);
|
|
|
|
context.Response.ContentLength = response.Length;
|
|
await context.Response.WriteAsync(response);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
$"Content-Length: {response.Length}",
|
|
"",
|
|
"hello, world");
|
|
|
|
// Wait for all callbacks to be called.
|
|
await onStartingTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
|
|
}
|
|
}
|
|
|
|
Assert.Equal(1, callOrder.Pop());
|
|
Assert.Equal(2, callOrder.Pop());
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task OnCompletedCallbacksAreCalledInLastInFirstOutOrder(ListenOptions listenOptions)
|
|
{
|
|
const string response = "hello, world";
|
|
|
|
var testContext = new TestServiceContext();
|
|
|
|
var callOrder = new Stack<int>();
|
|
var onCompletedTcs = new TaskCompletionSource<object>();
|
|
|
|
using (var server = new TestServer(async context =>
|
|
{
|
|
context.Response.OnCompleted(_ =>
|
|
{
|
|
callOrder.Push(1);
|
|
onCompletedTcs.SetResult(null);
|
|
return TaskCache.CompletedTask;
|
|
}, null);
|
|
context.Response.OnCompleted(_ =>
|
|
{
|
|
callOrder.Push(2);
|
|
return TaskCache.CompletedTask;
|
|
}, null);
|
|
|
|
context.Response.ContentLength = response.Length;
|
|
await context.Response.WriteAsync(response);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"",
|
|
"");
|
|
await connection.ReceiveEnd(
|
|
"HTTP/1.1 200 OK",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
$"Content-Length: {response.Length}",
|
|
"",
|
|
"hello, world");
|
|
|
|
// Wait for all callbacks to be called.
|
|
await onCompletedTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10));
|
|
}
|
|
}
|
|
|
|
Assert.Equal(1, callOrder.Pop());
|
|
Assert.Equal(2, callOrder.Pop());
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(ConnectionAdapterData))]
|
|
public async Task UpgradeRequestIsNotKeptAliveOrChunked(ListenOptions listenOptions)
|
|
{
|
|
const string message = "Hello World";
|
|
|
|
var testContext = new TestServiceContext();
|
|
|
|
using (var server = new TestServer(async context =>
|
|
{
|
|
var upgradeFeature = context.Features.Get<IHttpUpgradeFeature>();
|
|
var duplexStream = await upgradeFeature.UpgradeAsync();
|
|
|
|
var buffer = new byte[message.Length];
|
|
var read = 0;
|
|
while (read < message.Length)
|
|
{
|
|
read += await duplexStream.ReadAsync(buffer, read, buffer.Length - read).TimeoutAfter(TimeSpan.FromSeconds(10));
|
|
}
|
|
|
|
await duplexStream.WriteAsync(buffer, 0, read);
|
|
}, testContext, listenOptions))
|
|
{
|
|
using (var connection = server.CreateConnection())
|
|
{
|
|
await connection.Send(
|
|
"GET / HTTP/1.1",
|
|
"Connection: Upgrade",
|
|
"",
|
|
message);
|
|
await connection.ReceiveForcedEnd(
|
|
"HTTP/1.1 101 Switching Protocols",
|
|
"Connection: Upgrade",
|
|
$"Date: {testContext.DateHeaderValue}",
|
|
"",
|
|
message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|