File: ResponseHeaderTests.cs
Web Access
Project: src\src\Servers\HttpSys\test\FunctionalTests\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj (Microsoft.AspNetCore.Server.HttpSys.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.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Primitives;
using Xunit;
 
namespace Microsoft.AspNetCore.Server.HttpSys;
 
public class ResponseHeaderTests : LoggedTest
{
    [ConditionalFact]
    public async Task ResponseHeaders_ServerSendsDefaultHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            return Task.FromResult(0);
        }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.Equal(2, response.Headers.Count());
            Assert.False(response.Headers.TransferEncodingChunked.HasValue);
            Assert.True(response.Headers.Date.HasValue);
            Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers.Server.ToString());
            Assert.Single(response.Content.Headers);
            Assert.Equal(0, response.Content.Headers.ContentLength);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_ServerSendsSingleValueKnownHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
            var responseHeaders = responseInfo.Headers;
            responseHeaders["WWW-Authenticate"] = new string[] { "custom1" };
            return Task.FromResult(0);
        }, LoggerFactory))
        {
 
#pragma warning disable SYSLIB0014 // HttpClient would merge the headers no matter what
            WebRequest request = WebRequest.Create(address);
#pragma warning restore SYSLIB0014
            HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
            Assert.Equal(4, response.Headers.Count);
            Assert.Null(response.Headers["Transfer-Encoding"]);
            Assert.Equal(0, response.ContentLength);
            Assert.NotNull(response.Headers["Date"]);
            Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
            Assert.Equal("custom1", response.Headers["WWW-Authenticate"]);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_ServerSendsMultiValueKnownHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
            var responseHeaders = responseInfo.Headers;
            responseHeaders["WWW-Authenticate"] = new string[] { "custom1, and custom2", "custom3" };
            return Task.FromResult(0);
        }, LoggerFactory))
        {
#pragma warning disable SYSLIB0014 // HttpClient would merge the headers no matter what
            WebRequest request = WebRequest.Create(address);
#pragma warning restore SYSLIB0014
            HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
            Assert.Equal(4, response.Headers.Count);
            Assert.Null(response.Headers["Transfer-Encoding"]);
            Assert.Equal(0, response.ContentLength);
            Assert.NotNull(response.Headers["Date"]);
            Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
            Assert.Equal("custom1, and custom2, custom3", response.Headers["WWW-Authenticate"]);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_ServerSendsCustomHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
            var responseHeaders = responseInfo.Headers;
            responseHeaders["Custom-Header1"] = new string[] { "custom1, and custom2", "custom3" };
            return Task.FromResult(0);
        }, LoggerFactory))
        {
#pragma warning disable SYSLIB0014 // HttpClient would merge the headers no matter what
            WebRequest request = WebRequest.Create(address);
#pragma warning restore SYSLIB0014
            HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync();
            Assert.Equal(4, response.Headers.Count);
            Assert.Null(response.Headers["Transfer-Encoding"]);
            Assert.Equal(0, response.ContentLength);
            Assert.NotNull(response.Headers["Date"]);
            Assert.Equal("Microsoft-HTTPAPI/2.0", response.Headers["Server"]);
            Assert.Equal("custom1, and custom2, custom3", response.Headers["Custom-Header1"]);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_ServerSendsNonAsciiHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
            var responseHeaders = responseInfo.Headers;
            responseHeaders["Custom-Header1"] = new string[] { "Dašta" };
            return Task.FromResult(0);
        }, LoggerFactory))
        {
            var socketsHttpHandler = new SocketsHttpHandler() { ResponseHeaderEncodingSelector = (_, _) => Encoding.UTF8 };
            var httpClient = new HttpClient(socketsHttpHandler);
            var response = await httpClient.GetAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.True(response.Headers.TryGetValues("Custom-Header1", out var header));
            Assert.Equal("Dašta", header.Single());
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_ServerSendsConnectionClose_Closed()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
            var responseHeaders = responseInfo.Headers;
            responseHeaders["Connection"] = new string[] { "Close" };
            return httpContext.Response.Body.FlushAsync(); // Http.Sys adds the Content-Length: header for us if we don't flush
        }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.True(response.Headers.ConnectionClose.Value);
            Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
            Assert.True(response.Headers.TransferEncodingChunked.HasValue);
            Assert.True(response.Headers.TransferEncodingChunked);
            IEnumerable<string> values;
            var result = response.Content.Headers.TryGetValues("Content-Length", out values);
            Assert.False(result);
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_HTTP10Request_Gets11Close()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            return Task.FromResult(0);
        }, LoggerFactory))
        {
            using (HttpClient client = new HttpClient())
            {
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address);
                request.Version = new Version(1, 0);
                HttpResponseMessage response = await client.SendAsync(request);
                response.EnsureSuccessStatusCode();
                Assert.Equal(new Version(1, 1), response.Version);
                Assert.True(response.Headers.ConnectionClose.Value);
                Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
                Assert.False(response.Headers.TransferEncodingChunked.HasValue);
            }
        }
    }
 
    [ConditionalFact]
    public async Task ResponseHeaders_HTTP10RequestWithChunkedHeader_ManualChunking()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var response = httpContext.Response;
            var responseHeaders = response.Headers;
            responseHeaders["Transfer-Encoding"] = new string[] { "chunked" };
            var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
            return response.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
        }, LoggerFactory))
        {
            using (HttpClient client = new HttpClient())
            {
                HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, address);
                request.Version = new Version(1, 0);
                HttpResponseMessage response = await client.SendAsync(request);
                response.EnsureSuccessStatusCode();
                Assert.Equal(new Version(1, 1), response.Version);
                Assert.True(response.Headers.TransferEncodingChunked.HasValue);
                Assert.False(response.Content.Headers.Contains("Content-Length"));
                Assert.True(response.Headers.ConnectionClose.Value);
                Assert.Equal(new string[] { "close" }, response.Headers.GetValues("Connection"));
                Assert.Equal("Manually Chunked", await response.Content.ReadAsStringAsync());
            }
        }
    }
 
    [ConditionalFact]
    public async Task Headers_FlushSendsHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
            {
                httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
                var response = httpContext.Response;
                var responseHeaders = response.Headers;
                responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" });
                responseHeaders.Add("Custom2", new string[] { "value2a, value2b" });
                var body = response.Body;
                Assert.False(response.HasStarted);
                body.Flush();
                Assert.True(response.HasStarted);
                Assert.Throws<InvalidOperationException>(() => response.StatusCode = 404);
                Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
                return Task.FromResult(0);
            }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked
 
            Assert.Equal(2, response.Headers.GetValues("Custom1").Count());
            Assert.Equal("value1a", response.Headers.GetValues("Custom1").First());
            Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First());
            Assert.Single(response.Headers.GetValues("Custom2"));
            Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First());
        }
    }
 
    [ConditionalFact]
    public async Task Headers_FlushAsyncSendsHeaders_Success()
    {
        string address;
        using (Utilities.CreateHttpServer(out address, async httpContext =>
            {
                var response = httpContext.Response;
                var responseHeaders = response.Headers;
                responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" });
                responseHeaders.Add("Custom2", new string[] { "value2a, value2b" });
                var body = response.Body;
                Assert.False(response.HasStarted);
                await body.FlushAsync();
                Assert.True(response.HasStarted);
                Assert.Throws<InvalidOperationException>(() => response.StatusCode = 404);
                Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
            }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            Assert.Equal(5, response.Headers.Count()); // Date, Server, Chunked
 
            Assert.Equal(2, response.Headers.GetValues("Custom1").Count());
            Assert.Equal("value1a", response.Headers.GetValues("Custom1").First());
            Assert.Equal("value1b", response.Headers.GetValues("Custom1").Skip(1).First());
            Assert.Single(response.Headers.GetValues("Custom2"));
            Assert.Equal("value2a, value2b", response.Headers.GetValues("Custom2").First());
        }
    }
 
    [ConditionalTheory, MemberData(nameof(NullHeaderData))]
    public async Task Headers_IgnoreNullHeaders(string headerName, StringValues headerValue, StringValues expectedValue)
    {
        string address;
        using (Utilities.CreateHttpServer(out address, httpContext =>
        {
            var responseHeaders = httpContext.Response.Headers;
            responseHeaders.Add(headerName, headerValue);
            return Task.FromResult(0);
        }, LoggerFactory))
        {
            HttpResponseMessage response = await SendRequestAsync(address);
            response.EnsureSuccessStatusCode();
            var headers = response.Headers;
 
            if (expectedValue.Count == 0)
            {
                Assert.False(headers.Contains(headerName));
            }
            else
            {
                Assert.True(headers.Contains(headerName));
                Assert.Equal(headers.GetValues(headerName), expectedValue);
            }
        }
    }
 
    public static TheoryData<string, StringValues, StringValues> NullHeaderData
    {
        get
        {
            var dataset = new TheoryData<string, StringValues, StringValues>();
 
            // Unknown headers
            dataset.Add("NullString", (string)null, (string)null);
            dataset.Add("EmptyString", "", "");
            dataset.Add("NullStringArray", new string[] { null }, "");
            dataset.Add("EmptyStringArray", new string[] { "" }, "");
            dataset.Add("MixedStringArray", new string[] { null, "" }, new string[] { "", "" });
            // Known headers
            dataset.Add("Location", (string)null, (string)null);
            dataset.Add("Location", "", (string)null);
            dataset.Add("Location", new string[] { null }, (string)null);
            dataset.Add("Location", new string[] { "" }, (string)null);
            dataset.Add("Location", new string[] { "a" }, "a");
            dataset.Add("Location", new string[] { null, "" }, (string)null);
            dataset.Add("Location", new string[] { null, "", "a", "b" }, new string[] { "a", "b" });
 
            return dataset;
        }
    }
 
    private async Task<HttpResponseMessage> SendRequestAsync(string uri)
    {
        using (HttpClient client = new HttpClient())
        {
            return await client.GetAsync(uri);
        }
    }
}