File: ChunkedRequestTests.cs
Web Access
Project: src\src\Servers\Kestrel\test\InMemory.FunctionalTests\InMemory.FunctionalTests.csproj (InMemory.FunctionalTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Xunit;
using BadHttpRequestException = Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException;
 
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
 
public class ChunkedRequestTests : LoggedTest
{
    private async Task App(HttpContext httpContext)
    {
        var request = httpContext.Request;
        var response = httpContext.Response;
        Assert.True(request.CanHaveBody());
        while (true)
        {
            var buffer = new byte[8192];
            var count = await request.Body.ReadAsync(buffer, 0, buffer.Length);
            if (count == 0)
            {
                break;
            }
            await response.Body.WriteAsync(buffer, 0, count);
        }
    }
 
    private async Task PipeApp(HttpContext httpContext)
    {
        var request = httpContext.Request;
        var response = httpContext.Response;
        Assert.True(request.CanHaveBody());
        while (true)
        {
            var readResult = await request.BodyReader.ReadAsync();
            if (readResult.IsCompleted)
            {
                break;
            }
            // Need to copy here.
            await response.BodyWriter.WriteAsync(readResult.Buffer.ToArray());
 
            request.BodyReader.AdvanceTo(readResult.Buffer.End);
        }
    }
 
    private async Task AppChunked(HttpContext httpContext)
    {
        var request = httpContext.Request;
        var response = httpContext.Response;
        Assert.True(request.CanHaveBody());
        var data = new MemoryStream();
        await request.Body.CopyToAsync(data);
        var bytes = data.ToArray();
 
        response.Headers["Content-Length"] = bytes.Length.ToString(CultureInfo.InvariantCulture);
        await response.Body.WriteAsync(bytes, 0, bytes.Length);
    }
 
    [Fact]
    public async Task Http10TransferEncoding()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(App, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.Send(
                    "POST / HTTP/1.0",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "5", "Hello",
                    "6", " World",
                    "0",
                    "",
                    "");
                await connection.ReceiveEnd(
                    "HTTP/1.1 200 OK",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World");
            }
        }
    }
 
    [Fact]
    public async Task Http10TransferEncodingPipes()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(PipeApp, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.Send(
                    "POST / HTTP/1.0",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "5", "Hello",
                    "6", " World",
                    "0",
                    "",
                    "");
                await connection.ReceiveEnd(
                    "HTTP/1.1 200 OK",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World");
            }
        }
    }
 
    [Fact]
    public async Task Http10KeepAliveTransferEncoding()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(AppChunked, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.Send(
                    "POST / HTTP/1.0",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "Connection: keep-alive",
                    "",
                    "5", "Hello",
                    "6", " World",
                    "0",
                    "",
                    "POST / HTTP/1.0",
                    "Content-Length: 7",
                    "",
                    "Goodbye");
                await connection.Receive(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    "Connection: keep-alive",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World");
                await connection.ReceiveEnd(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 7",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Goodbye");
            }
        }
    }
 
    [Fact]
    public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
            Assert.True(request.CanHaveBody());
 
            Assert.Equal("POST", request.Method);
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.Send(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Content-Length: 5",
                    "",
                    "HelloPOST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "C", "HelloChunked",
                    "0",
                    "",
                    "POST / HTTP/1.1",
                    "Host:",
                    "Content-Length: 7",
                    "",
                    "Goodbye");
                await connection.Receive(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello WorldHTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello WorldHTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World");
            }
        }
    }
 
    [Fact]
    public async Task TrailingHeadersAreParsed()
    {
        var requestCount = 10;
        var requestsReceived = 0;
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
            Assert.True(request.CanHaveBody());
 
            var buffer = new byte[200];
 
            // The first request is chunked with no trailers.
            if (requestsReceived == 0)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
                Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));  // Not yet
            }
            // The middle requests are chunked with trailers.
            else if (requestsReceived < requestCount)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
                Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));  // Not yet
                Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
            }
            // The last request is content-length with no trailers.
            else
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));
            }
 
            while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
            {
                ;// read to end
            }
 
            Assert.False(request.Headers.ContainsKey("X-Trailer-Header"));
 
            // The first request is chunked with no trailers.
            if (requestsReceived == 0)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
                Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
            }
            // The middle requests are chunked with trailers.
            else if (requestsReceived < requestCount)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
                Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString());
            }
            // The last request is content-length with no trailers.
            else
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
                Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
            }
 
            requestsReceived++;
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
        }, new TestServiceContext(LoggerFactory)))
        {
            var response = string.Join("\r\n", new string[] {
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {server.Context.DateHeaderValue}",
                    "",
                    "Hello World"});
 
            var expectedFullResponse = string.Join("", Enumerable.Repeat(response, requestCount + 1));
 
            IEnumerable<string> sendSequence = new string[] {
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "C",
                    "HelloChunked",
                    "0",
                    ""};
 
            for (var i = 1; i < requestCount; i++)
            {
                sendSequence = sendSequence.Concat(new string[] {
                        "POST / HTTP/1.1",
                        "Host:",
                        "Transfer-Encoding: chunked",
                        "Trailer: X-Trailer-Header",
                        "",
                        "C",
                        $"HelloChunk{i:00}",
                        "0",
                        string.Concat("X-Trailer-Header: ", new string('a', i)),
                        "" });
            }
 
            sendSequence = sendSequence.Concat(new string[] {
                    "POST / HTTP/1.1",
                    "Host:",
                    "Content-Length: 7",
                    "",
                    "Goodbye"
                });
 
            var fullRequest = sendSequence.ToArray();
 
            using (var connection = server.CreateConnection())
            {
                await connection.Send(fullRequest);
                await connection.Receive(expectedFullResponse);
            }
        }
    }
 
    [Fact]
    public async Task TrailingHeadersAreParsedWithPipe()
    {
        var requestCount = 10;
        var requestsReceived = 0;
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
            Assert.True(request.CanHaveBody());
 
            // The first request is chunked with no trailers.
            if (requestsReceived == 0)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
                Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));  // Not yet
            }
            // The middle requests are chunked with trailers.
            else if (requestsReceived < requestCount)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable"); // Not yet
                Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));  // Not yet
                Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
            }
            // The last request is content-length with no trailers.
            else
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.False(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Throws<InvalidOperationException>(() => request.GetTrailer("X-Trailer-Header"));
            }
 
            while (true)
            {
                var result = await request.BodyReader.ReadAsync();
                request.BodyReader.AdvanceTo(result.Buffer.End);
                if (result.IsCompleted)
                {
                    break;
                }
            }
 
            Assert.False(request.Headers.ContainsKey("X-Trailer-Header"));
 
            // The first request is chunked with no trailers.
            if (requestsReceived == 0)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
                Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
            }
            // The middle requests are chunked with trailers.
            else if (requestsReceived < requestCount)
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Equal("X-Trailer-Header", request.GetDeclaredTrailers().ToString());
                Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString());
            }
            // The last request is content-length with no trailers.
            else
            {
                Assert.True(request.SupportsTrailers(), "SupportsTrailers");
                Assert.True(request.CheckTrailersAvailable(), "CheckTrailersAvailable");
                Assert.Equal(string.Empty, request.GetDeclaredTrailers().ToString());
                Assert.Equal(string.Empty, request.GetTrailer("X-Trailer-Header").ToString());
            }
 
            requestsReceived++;
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
        }, new TestServiceContext(LoggerFactory)))
        {
            var response = string.Join("\r\n", new string[] {
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {server.Context.DateHeaderValue}",
                    "",
                    "Hello World"});
 
            var expectedFullResponse = string.Join("", Enumerable.Repeat(response, requestCount + 1));
 
            IEnumerable<string> sendSequence = new string[] {
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "C",
                    "HelloChunked",
                    "0",
                    ""};
 
            for (var i = 1; i < requestCount; i++)
            {
                sendSequence = sendSequence.Concat(new string[] {
                        "POST / HTTP/1.1",
                        "Host:",
                        "Transfer-Encoding: chunked",
                        "Trailer: X-Trailer-Header",
                        "",
                        "C",
                        $"HelloChunk{i:00}",
                        "0",
                        string.Concat("X-Trailer-Header: ", new string('a', i)),
                        "" });
            }
 
            sendSequence = sendSequence.Concat(new string[] {
                    "POST / HTTP/1.1",
                    "Host:",
                    "Content-Length: 7",
                    "",
                    "Goodbye"
                });
 
            var fullRequest = sendSequence.ToArray();
 
            using (var connection = server.CreateConnection())
            {
                await connection.Send(fullRequest);
                await connection.Receive(expectedFullResponse);
            }
        }
    }
    [Fact]
    public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit()
    {
        const string transferEncodingHeaderLine = "Transfer-Encoding: chunked";
        const string headerLine = "Header: value";
        const string trailingHeaderLine = "Trailing-Header: trailing-value";
 
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions.Limits.MaxRequestHeadersTotalSize =
            transferEncodingHeaderLine.Length + 2 +
            headerLine.Length + 2 +
            trailingHeaderLine.Length + 1;
 
        await using (var server = new TestServer(async context =>
        {
            var buffer = new byte[128];
            while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
            {
                // read to end
            }
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    $"{transferEncodingHeaderLine}",
                    $"{headerLine}",
                    "",
                    "2",
                    "42",
                    "0",
                    $"{trailingHeaderLine}",
                    "",
                    "");
                await connection.ReceiveEnd(
                    "HTTP/1.1 431 Request Header Fields Too Large",
                    "Content-Length: 0",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
    }
 
    [Fact]
    public async Task TrailingHeadersCountTowardsHeaderCountLimit()
    {
        const string transferEncodingHeaderLine = "Transfer-Encoding: chunked";
        const string headerLine = "Header: value";
        const string trailingHeaderLine = "Trailing-Header: trailing-value";
 
        var testContext = new TestServiceContext(LoggerFactory);
        testContext.ServerOptions.Limits.MaxRequestHeaderCount = 2;
 
        await using (var server = new TestServer(async context =>
        {
            var buffer = new byte[128];
            while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
            {
                // read to end
            }
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    $"{transferEncodingHeaderLine}",
                    $"{headerLine}",
                    "",
                    "2",
                    "42",
                    "0",
                    $"{trailingHeaderLine}",
                    "",
                    "");
                await connection.ReceiveEnd(
                    "HTTP/1.1 431 Request Header Fields Too Large",
                    "Content-Length: 0",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
    }
 
    [Fact]
    public async Task ExtensionsAreIgnored()
    {
        var testContext = new TestServiceContext(LoggerFactory);
        var requestCount = 10;
        var requestsReceived = 0;
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
 
            var buffer = new byte[200];
 
            while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
            {
                ;// read to end
            }
 
            Assert.True(string.IsNullOrEmpty(request.Headers["X-Trailer-Header"]));
 
            if (requestsReceived < requestCount)
            {
                Assert.Equal(new string('a', requestsReceived), request.GetTrailer("X-Trailer-Header").ToString());
            }
 
            requestsReceived++;
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
        }, testContext))
        {
            var response = string.Join("\r\n", new string[] {
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World"});
 
            var expectedFullResponse = string.Join("", Enumerable.Repeat(response, requestCount + 1));
 
            IEnumerable<string> sendSequence = new string[] {
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "C;hello there",
                    "HelloChunked",
                    "0;hello there",
                    ""};
 
            for (var i = 1; i < requestCount; i++)
            {
                sendSequence = sendSequence.Concat(new string[] {
                        "POST / HTTP/1.1",
                        "Host:",
                        "Transfer-Encoding: chunked",
                        "",
                        "C;hello there",
                        $"HelloChunk{i:00}",
                        "0;hello there",
                        string.Concat("X-Trailer-Header: ", new string('a', i)),
                        "" });
            }
 
            sendSequence = sendSequence.Concat(new string[] {
                    "POST / HTTP/1.1",
                    "Host:",
                    "Content-Length: 7",
                    "",
                    "Goodbye"
                });
 
            var fullRequest = sendSequence.ToArray();
 
            using (var connection = server.CreateConnection())
            {
                await connection.Send(fullRequest);
                await connection.Receive(expectedFullResponse);
            }
        }
    }
 
    [Fact]
    public async Task InvalidLengthResultsIn400()
    {
        var testContext = new TestServiceContext(LoggerFactory);
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
            Assert.True(request.CanHaveBody());
 
            var buffer = new byte[200];
 
            while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
            {
                ;// read to end
            }
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "Cii");
 
                await connection.Receive(
                    "HTTP/1.1 400 Bad Request",
                    "Content-Length: 0",
                    "Connection: close",
                    "");
                await connection.ReceiveEnd(
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
    }
 
    [Fact]
    public async Task InvalidSizedDataResultsIn400()
    {
        var testContext = new TestServiceContext(LoggerFactory);
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
            Assert.True(request.CanHaveBody());
 
            var buffer = new byte[200];
 
            while (await request.Body.ReadAsync(buffer, 0, buffer.Length) != 0)
            {
                ;// read to end
            }
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.Body.WriteAsync(Encoding.ASCII.GetBytes("Hello World"), 0, 11);
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "C",
                    "HelloChunkedIn");
 
                await connection.Receive(
                    "HTTP/1.1 400 Bad Request",
                    "Content-Length: 0",
                    "Connection: close",
                    "");
                await connection.ReceiveEnd(
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
    }
 
    [Fact]
    public async Task ChunkedNotFinalTransferCodingResultsIn400()
    {
        var testContext = new TestServiceContext(LoggerFactory);
        await using (var server = new TestServer(httpContext =>
        {
            return Task.CompletedTask;
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: not-chunked",
                    "",
                    "C",
                    "hello, world",
                    "0",
                    "",
                    "");
 
                await connection.ReceiveEnd(
                    "HTTP/1.1 400 Bad Request",
                    "Content-Length: 0",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
 
            // Content-Length should not affect this
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: not-chunked",
                    "Content-Length: 22",
                    "",
                    "C",
                    "hello, world",
                    "0",
                    "",
                    "");
 
                await connection.ReceiveEnd(
                    "HTTP/1.1 400 Bad Request",
                    "Content-Length: 0",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
 
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked, not-chunked",
                    "",
                    "C",
                    "hello, world",
                    "0",
                    "",
                    "");
 
                await connection.ReceiveEnd(
                    "HTTP/1.1 400 Bad Request",
                    "Content-Length: 0",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
 
            // Content-Length should not affect this
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked, not-chunked",
                    "Content-Length: 22",
                    "",
                    "C",
                    "hello, world",
                    "0",
                    "",
                    "");
 
                await connection.ReceiveEnd(
                    "HTTP/1.1 400 Bad Request",
                    "Content-Length: 0",
                    "Connection: close",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
    }
 
    [Fact]
    public async Task ClosingConnectionMidChunkPrefixThrows()
    {
        var testContext = new TestServiceContext(LoggerFactory);
        var readStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
#pragma warning disable CS0618 // Type or member is obsolete
        var exTcs = new TaskCompletionSource<BadHttpRequestException>(TaskCreationOptions.RunContinuationsAsynchronously);
#pragma warning restore CS0618 // Type or member is obsolete
 
        await using (var server = new TestServer(async httpContext =>
        {
            var readTask = httpContext.Request.Body.CopyToAsync(Stream.Null);
            readStartedTcs.SetResult();
 
            try
            {
                await readTask;
            }
#pragma warning disable CS0618 // Type or member is obsolete
            catch (BadHttpRequestException badRequestEx)
#pragma warning restore CS0618 // Type or member is obsolete
            {
                exTcs.TrySetResult(badRequestEx);
            }
            catch (Exception ex)
            {
                exTcs.SetException(ex);
            }
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "1");
 
                await readStartedTcs.Task.TimeoutAfter(TestConstants.DefaultTimeout);
 
                connection.ShutdownSend();
 
                await connection.ReceiveEnd();
 
                var badReqEx = await exTcs.Task.TimeoutAfter(TestConstants.DefaultTimeout);
                Assert.Equal(RequestRejectionReason.UnexpectedEndOfRequestContent, badReqEx.Reason);
            }
        }
    }
 
    [Fact]
    public async Task ChunkedRequestCallCancelPendingReadWorks()
    {
        var tcs = new TaskCompletionSource();
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
 
            Assert.Equal("POST", request.Method);
 
            var readResult = await request.BodyReader.ReadAsync();
            request.BodyReader.AdvanceTo(readResult.Buffer.End);
 
            var requestTask = httpContext.Request.BodyReader.ReadAsync();
 
            httpContext.Request.BodyReader.CancelPendingRead();
 
            Assert.True((await requestTask).IsCanceled);
 
            tcs.SetResult();
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.BodyWriter.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
 
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "1",
                    "H");
                await tcs.Task;
                await connection.Send(
                    "4",
                    "ello",
                    "0",
                    "",
                    "");
 
                await connection.Receive(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World");
            }
        }
    }
 
    [Fact]
    public async Task ChunkedRequestCallCompleteThrowsExceptionOnRead()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
 
            Assert.Equal("POST", request.Method);
 
            var readResult = await request.BodyReader.ReadAsync();
            request.BodyReader.AdvanceTo(readResult.Buffer.End);
 
            httpContext.Request.BodyReader.Complete();
 
            await Assert.ThrowsAsync<InvalidOperationException>(async () => await request.BodyReader.ReadAsync());
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.BodyWriter.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
 
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.Send(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "1",
                    "H",
                    "4",
                    "ello",
                    "0",
                    "",
                    "");
 
                await connection.Receive(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 11",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "Hello World");
            }
        }
    }
 
    [Fact]
    public async Task ChunkedRequestCallCompleteDoesNotCauseException()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(async httpContext =>
        {
            var request = httpContext.Request;
 
            // This read may receive all data, but what we care about
            // is that ConsumeAsync is called and doesn't error. Calling
            // TryRead before would always fail.
            var readResult = await request.BodyReader.ReadAsync();
            request.BodyReader.AdvanceTo(readResult.Buffer.End);
 
            request.BodyReader.Complete();
 
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.Send(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "1",
                    "H",
                    "4",
                    "ello",
                    "0",
                    "",
                    "");
 
                await connection.Receive(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 0",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
 
                // start another request to make sure OnComsumeAsync is hit
                await connection.Send(
                   "POST / HTTP/1.1",
                   "Host:",
                   "Transfer-Encoding: chunked",
                   "",
                   "0",
                   "",
                   "");
 
                await connection.Receive(
                    "HTTP/1.1 200 OK",
                    "Content-Length: 0",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
 
        Assert.All(TestSink.Writes, w => Assert.InRange(w.LogLevel, LogLevel.Trace, LogLevel.Information));
    }
 
    [Fact]
    public async Task ChunkedRequestCallCompleteWithExceptionCauses500()
    {
        var testContext = new TestServiceContext(LoggerFactory);
 
        await using (var server = new TestServer(async httpContext =>
        {
            var response = httpContext.Response;
            var request = httpContext.Request;
 
            Assert.Equal("POST", request.Method);
 
            var readResult = await request.BodyReader.ReadAsync();
            request.BodyReader.AdvanceTo(readResult.Buffer.End);
 
            httpContext.Request.BodyReader.Complete(new Exception());
 
            response.Headers["Content-Length"] = new[] { "11" };
 
            await response.BodyWriter.WriteAsync(new Memory<byte>(Encoding.ASCII.GetBytes("Hello World"), 0, 11));
 
        }, testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendAll(
                    "POST / HTTP/1.1",
                    "Host:",
                    "Transfer-Encoding: chunked",
                    "",
                    "1",
                    "H",
                    "0",
                    "",
                    "");
 
                await connection.Receive(
                    "HTTP/1.1 500 Internal Server Error",
                    "Content-Length: 0",
                    $"Date: {testContext.DateHeaderValue}",
                    "",
                    "");
            }
        }
    }
}