From 3c0308fa6e5d65287abb9d07d7d79f8126bfc984 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Mon, 16 Dec 2019 19:42:03 -0800 Subject: [PATCH] Add HTTP/2 interop tests for HttpClient and Kestrel (#17875) --- .../HttpClientHttp2InteropTests.cs | 1135 ++++++++++++++++- 1 file changed, 1111 insertions(+), 24 deletions(-) diff --git a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs index 234c127379..9c1313e760 100644 --- a/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs +++ b/src/Servers/Kestrel/test/Interop.FunctionalTests/HttpClientHttp2InteropTests.cs @@ -1,9 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Http; using System.Text; @@ -41,7 +42,6 @@ namespace Interop.FunctionalTests new[] { "http" } }; - // https://github.com/aspnet/AspNetCore/issues/11301 We should use Skip but it's broken at the moment. if (Utilities.CurrentPlatformSupportsAlpn()) { list.Add(new[] { "https" }); @@ -51,7 +51,7 @@ namespace Interop.FunctionalTests } } - [ConditionalTheory] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task HelloWorld(string scheme) { @@ -72,7 +72,7 @@ namespace Interop.FunctionalTests await host.StopAsync().DefaultTimeout(); } - [ConditionalTheory] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task Echo(string scheme) { @@ -102,7 +102,7 @@ namespace Interop.FunctionalTests } // Concurrency testing - [ConditionalTheory] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task MultiplexGet(string scheme) { @@ -150,7 +150,7 @@ namespace Interop.FunctionalTests } // Concurrency testing - [ConditionalTheory] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task MultiplexEcho(string scheme) { @@ -260,7 +260,7 @@ namespace Interop.FunctionalTests } } - [ConditionalTheory] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task BidirectionalStreaming(string scheme) { @@ -318,7 +318,7 @@ namespace Interop.FunctionalTests await host.StopAsync().DefaultTimeout(); } - [ConditionalTheory(Skip = "https://github.com/dotnet/corefx/issues/39404")] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task BidirectionalStreamingMoreClientData(string scheme) { @@ -388,7 +388,7 @@ namespace Interop.FunctionalTests await ReadStreamHelloWorld(stream); Assert.Equal(0, await stream.ReadAsync(new byte[10], 0, 10).DefaultTimeout()); - stream.Dispose(); // https://github.com/dotnet/corefx/issues/39404 can be worked around by commenting out this Dispose + stream.Dispose(); // Send one more message after the server has finished. await streamingContent.SendAsync("Hello World").DefaultTimeout(); @@ -400,7 +400,7 @@ namespace Interop.FunctionalTests await host.StopAsync().DefaultTimeout(); } - [ConditionalTheory(Skip = "https://github.com/dotnet/corefx/issues/39404")] + [Theory] [MemberData(nameof(SupportedSchemes))] public async Task ReverseEcho(string scheme) { @@ -412,16 +412,12 @@ namespace Interop.FunctionalTests webHostBuilder.ConfigureServices(AddTestLogging) .Configure(app => app.Run(async context => { - // Prime it? - // var readTask = context.Request.BodyReader.ReadAsync(); context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("Hello World"); await context.Response.CompleteAsync().DefaultTimeout(); try { - // var readResult = await readTask; - // context.Request.BodyReader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); using var streamReader = new StreamReader(context.Request.Body); var read = await streamReader.ReadToEndAsync().DefaultTimeout(); clientEcho.SetResult(read); @@ -437,7 +433,8 @@ namespace Interop.FunctionalTests var url = host.MakeUrl(scheme); using var client = CreateClient(); - // client.DefaultRequestHeaders.ExpectContinue = true; + // The client doesn't flush the headers for requests with a body unless a continue is expected. + client.DefaultRequestHeaders.ExpectContinue = true; var streamingContent = new StreamingContent(); var request = CreateRequestMessage(HttpMethod.Post, url, streamingContent); @@ -446,15 +443,8 @@ namespace Interop.FunctionalTests Assert.Equal(HttpVersion.Version20, response.Version); // Read Hello World and echo it back to the server. - /* https://github.com/dotnet/corefx/issues/39404 var read = await response.Content.ReadAsStringAsync().DefaultTimeout(); Assert.Equal("Hello World", read); - */ - var stream = await response.Content.ReadAsStreamAsync().DefaultTimeout(); - await ReadStreamHelloWorld(stream).DefaultTimeout(); - - Assert.Equal(0, await stream.ReadAsync(new byte[10], 0, 10).DefaultTimeout()); - stream.Dispose(); // https://github.com/dotnet/corefx/issues/39404 can be worked around by commenting out this Dispose await streamingContent.SendAsync("Hello World").DefaultTimeout(); streamingContent.Complete(); @@ -514,6 +504,1095 @@ namespace Interop.FunctionalTests length = 0; return false; } + + internal void Abort() + { + if (_sendComplete == null) + { + throw new InvalidOperationException("Sending hasn't started yet."); + } + _sendComplete.TrySetException(new Exception("Abort")); + } + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ResponseTrailersWithoutData(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + context.Response.DeclareTrailer("TestTrailer"); + context.Response.AppendTrailer("TestTrailer", "TestValue"); + return Task.CompletedTask; + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var response = await client.GetAsync(url).DefaultTimeout(); + Assert.Equal(HttpVersion.Version20, response.Version); + Assert.Equal("TestTrailer", response.Headers.Trailer.Single()); + // The response is buffered, we must already have the trailers. + Assert.Equal("TestValue", response.TrailingHeaders.GetValues("TestTrailer").Single()); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ResponseTrailersWithData(string scheme) + { + var headersReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + context.Response.DeclareTrailer("TestTrailer"); + await context.Response.WriteAsync("Hello "); + await headersReceived.Task.DefaultTimeout(); + await context.Response.WriteAsync("World"); + context.Response.AppendTrailer("TestTrailer", "TestValue"); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + Assert.Equal(HttpVersion.Version20, response.Version); + Assert.Equal("TestTrailer", response.Headers.Trailer.Single()); + // The server has not sent trailers yet. + Assert.False(response.TrailingHeaders.TryGetValues("TestTrailer", out var none)); + headersReceived.SetResult(0); + var responseBody = await response.Content.ReadAsStringAsync().DefaultTimeout(); + Assert.Equal("Hello World", responseBody); + // The response is buffered, we must already have the trailers. + Assert.Equal("TestValue", response.TrailingHeaders.GetValues("TestTrailer").Single()); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ServerReset_BeforeResponse_ClientThrows(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + context.Features.Get().Reset(8); // Cancel + return Task.CompletedTask; + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var exception = await Assert.ThrowsAsync(() => client.GetAsync(url)).DefaultTimeout(); + Assert.Equal("The HTTP/2 server reset the stream. HTTP/2 error code 'CANCEL' (0x8).", exception?.InnerException?.InnerException.Message); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ServerReset_AfterHeaders_ClientBodyThrows(string scheme) + { + var receivedHeaders = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + await context.Response.BodyWriter.FlushAsync(); + await receivedHeaders.Task.DefaultTimeout(); + context.Features.Get().Reset(8); // Cancel + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + receivedHeaders.SetResult(0); + var exception = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync()).DefaultTimeout(); + Assert.Equal("The HTTP/2 server reset the stream. HTTP/2 error code 'CANCEL' (0x8).", exception?.InnerException?.InnerException.Message); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ServerReset_AfterEndStream_NoError(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + await context.Response.WriteAsync("Hello World"); + await context.Response.CompleteAsync(); + context.Features.Get().Reset(8); // Cancel + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + var body = await response.Content.ReadAsStringAsync().DefaultTimeout(); + Assert.Equal("Hello World", body); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ServerReset_AfterTrailers_NoError(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + context.Response.DeclareTrailer("TestTrailer"); + await context.Response.WriteAsync("Hello World"); + context.Response.AppendTrailer("TestTrailer", "TestValue"); + await context.Response.CompleteAsync(); + context.Features.Get().Reset(8); // Cancel + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + Assert.Equal(HttpVersion.Version20, response.Version); + Assert.Equal("TestTrailer", response.Headers.Trailer.Single()); + var responseBody = await response.Content.ReadAsStringAsync().DefaultTimeout(); + Assert.Equal("Hello World", responseBody); + // The response is buffered, we must already have the trailers. + Assert.Equal("TestValue", response.TrailingHeaders.GetValues("TestTrailer").Single()); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ServerReset_BeforeRequestBody_ClientBodyThrows(string scheme) + { + var clientEcho = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverReset = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var headersReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + context.Response.ContentType = "text/plain"; + await context.Response.BodyWriter.FlushAsync(); + await headersReceived.Task.DefaultTimeout(); + context.Features.Get().Reset(8); // Cancel + serverReset.SetResult(0); + + try + { + using var streamReader = new StreamReader(context.Request.Body); + var read = await streamReader.ReadToEndAsync().DefaultTimeout(); + clientEcho.SetResult(read); + } + catch (Exception ex) + { + clientEcho.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + // The client doesn't flush the headers for requests with a body unless a continue is expected. + client.DefaultRequestHeaders.ExpectContinue = true; + + var streamingContent = new StreamingContent(); + var request = CreateRequestMessage(HttpMethod.Post, url, streamingContent); + using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + headersReceived.SetResult(0); + + Assert.Equal(HttpVersion.Version20, response.Version); + + await serverReset.Task.DefaultTimeout(); + var responseEx = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync().DefaultTimeout()); + Assert.Contains("The HTTP/2 server reset the stream. HTTP/2 error code 'CANCEL' (0x8)", responseEx.ToString()); + await Assert.ThrowsAsync(() => streamingContent.SendAsync("Hello World").DefaultTimeout()); + await Assert.ThrowsAnyAsync(() => clientEcho.Task.DefaultTimeout()); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ServerReset_BeforeRequestBodyEnd_ClientBodyThrows(string scheme) + { + var clientEcho = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverReset = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var responseHeadersReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + var count = await context.Request.Body.ReadAsync(new byte[11], 0, 11); + Assert.Equal(11, count); + + context.Response.ContentType = "text/plain"; + await context.Response.BodyWriter.FlushAsync(); + await responseHeadersReceived.Task.DefaultTimeout(); + context.Features.Get().Reset(8); // Cancel + serverReset.SetResult(0); + + try + { + using var streamReader = new StreamReader(context.Request.Body); + var read = await streamReader.ReadToEndAsync().DefaultTimeout(); + clientEcho.SetResult(read); + } + catch (Exception ex) + { + clientEcho.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var streamingContent = new StreamingContent(); + var request = CreateRequestMessage(HttpMethod.Post, url, streamingContent); + var requestTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + await streamingContent.SendAsync("Hello World").DefaultTimeout(); + using var response = await requestTask; + responseHeadersReceived.SetResult(0); + + Assert.Equal(HttpVersion.Version20, response.Version); + + await serverReset.Task.DefaultTimeout(); + var responseEx = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync().DefaultTimeout()); + Assert.Contains("The HTTP/2 server reset the stream. HTTP/2 error code 'CANCEL' (0x8)", responseEx.ToString()); + await Assert.ThrowsAsync(() => streamingContent.SendAsync("Hello World").DefaultTimeout()); + await Assert.ThrowsAnyAsync(() => clientEcho.Task.DefaultTimeout()); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ClientReset_BeforeRequestData_ReadThrows(string scheme) + { + var requestReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + try + { + var readTask = context.Request.Body.ReadAsync(new byte[11], 0, 11); + requestReceived.SetResult(0); + var ex = await Assert.ThrowsAsync(() => readTask).DefaultTimeout(); + serverResult.SetResult(0); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + // The client doesn't flush the headers for requests with a body unless a continue is expected. + client.DefaultRequestHeaders.ExpectContinue = true; + + var streamingContent = new StreamingContent(); + var request = CreateRequestMessage(HttpMethod.Post, url, streamingContent); + var requestTask = client.SendAsync(request); + await requestReceived.Task.DefaultTimeout(); + streamingContent.Abort(); + await serverResult.Task.DefaultTimeout(); + await Assert.ThrowsAnyAsync(() => requestTask).DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ClientReset_BeforeRequestDataEnd_ReadThrows(string scheme) + { + var requestReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + try + { + await ReadStreamHelloWorld(context.Request.Body); + var readTask = context.Request.Body.ReadAsync(new byte[11], 0, 11); + requestReceived.SetResult(0); + var ex = await Assert.ThrowsAsync(() => readTask).DefaultTimeout(); + serverResult.SetResult(0); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var streamingContent = new StreamingContent(); + var request = CreateRequestMessage(HttpMethod.Post, url, streamingContent); + var requestTask = client.SendAsync(request); + await streamingContent.SendAsync("Hello World").DefaultTimeout(); + await requestReceived.Task.DefaultTimeout(); + streamingContent.Abort(); + await serverResult.Task.DefaultTimeout(); + await Assert.ThrowsAnyAsync(() => requestTask).DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ClientReset_BeforeResponse_ResponseSuppressed(string scheme) + { + var requestReceived = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + try + { + context.RequestAborted.Register(() => serverResult.SetResult(0)); + requestReceived.SetResult(0); + await serverResult.Task.DefaultTimeout(); + await context.Response.WriteAsync("Hello World").DefaultTimeout(); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var requestCancellation = new CancellationTokenSource(); + var requestTask = client.GetAsync(url, requestCancellation.Token); + await requestReceived.Task.DefaultTimeout(); + requestCancellation.Cancel(); + await serverResult.Task.DefaultTimeout(); + await Assert.ThrowsAsync(() => requestTask).DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ClientReset_BeforeEndStream_WritesSuppressed(string scheme) + { + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + try + { + context.RequestAborted.Register(() => serverResult.SetResult(0)); + await context.Response.WriteAsync("Hello World").DefaultTimeout(); + await serverResult.Task.DefaultTimeout(); + await context.Response.WriteAsync("Hello World").DefaultTimeout(); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + var responseStream = await response.Content.ReadAsStreamAsync().DefaultTimeout(); + await ReadStreamHelloWorld(responseStream); + responseStream.Dispose(); // Sends reset + await serverResult.Task.DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ClientReset_BeforeTrailers_TrailersSuppressed(string scheme) + { + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + try + { + context.RequestAborted.Register(() => serverResult.SetResult(0)); + await context.Response.WriteAsync("Hello World").DefaultTimeout(); + await serverResult.Task.DefaultTimeout(); + context.Response.AppendTrailer("foo", "bar"); + await context.Response.CompleteAsync(); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).DefaultTimeout(); + var responseStream = await response.Content.ReadAsStreamAsync().DefaultTimeout(); + await ReadStreamHelloWorld(responseStream); + responseStream.Dispose(); // Sends reset + await serverResult.Task.DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [Flaky("https://github.com/dotnet/runtime/issues/860", FlakyOn.All)] + [MemberData(nameof(SupportedSchemes))] + public async Task RequestHeaders_MultipleFrames_Accepted(string scheme) + { + var oneKbString = new string('a', 1024); + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + try + { + for (var i = 0; i < 20; i++) + { + Assert.Equal(oneKbString + i, context.Request.Headers["header" + i]); + } + serverResult.SetResult(0); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + return Task.CompletedTask; + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var request = CreateRequestMessage(HttpMethod.Get, url, content: null); + // The default frame size limit is 16kb, and the total header size limit is 32kb. + for (var i = 0; i < 20; i++) + { + request.Headers.Add("header" + i, oneKbString + i); + } + request.Headers.Host = "localhost"; // The default Host header has a random port value wich can cause the length to vary. + var requestTask = client.SendAsync(request); + var response = await requestTask.DefaultTimeout(); + await serverResult.Task.DefaultTimeout(); + response.EnsureSuccessStatusCode(); + + Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("received HEADERS frame for stream ID 1 with length 16384 and flags END_STREAM"))); + Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("received CONTINUATION frame for stream ID 1 with length 4390 and flags END_HEADERS"))); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ResponseHeaders_MultipleFrames_Accepted(string scheme) + { + var oneKbString = new string('a', 1024); + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + try + { + // The default frame size limit is 16kb, and the total header size limit is 64kb. + for (var i = 0; i < 59; i++) + { + context.Response.Headers.Append("header" + i, oneKbString + i); + } + serverResult.SetResult(0); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + return Task.CompletedTask; + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + var response = await client.GetAsync(url).DefaultTimeout(); + await serverResult.Task.DefaultTimeout(); + response.EnsureSuccessStatusCode(); + for (var i = 0; i < 59; i++) + { + Assert.Equal(oneKbString + i, response.Headers.GetValues("header" + i).Single()); + } + + Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending HEADERS frame for stream ID 1 with length 15636 and flags END_STREAM"))); + Assert.Equal(2, TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 15585 and flags NONE")).Count()); + Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("sending CONTINUATION frame for stream ID 1 with length 14546 and flags END_HEADERS"))); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + // Expect this to change when the client implements dynamic request header compression. + // Will the client send the first headers before receiving our settings frame? + // We'll probobly need to ensure the settings changes are ack'd before enforcing them. + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_HeaderTableSize_CanBeReduced_Server(string scheme) + { + var oneKbString = new string('a', 1024); + var serverResult = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureKestrel(options => + { + // Must be larger than 0, should disable header compression + options.Limits.Http2.HeaderTableSize = 1; + }); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + try + { + for (var i = 0; i < 14; i++) + { + Assert.Equal(oneKbString + i, context.Request.Headers["header" + i]); + } + serverResult.SetResult(0); + } + catch (Exception ex) + { + serverResult.SetException(ex); + } + return Task.CompletedTask; + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var request = CreateRequestMessage(HttpMethod.Get, url, content: null); + // The default frame size limit is 16kb, and the total header size limit is 32kb. + for (var i = 0; i < 14; i++) + { + request.Headers.Add("header" + i, oneKbString + i); + } + request.Headers.Host = "localhost"; // The default Host has a random port value that causes the length to very. + var requestTask = client.SendAsync(request); + var response = await requestTask.DefaultTimeout(); + await serverResult.Task.DefaultTimeout(); + response.EnsureSuccessStatusCode(); + + Assert.Single(TestSink.Writes.Where(context + => context.Message.Contains("received HEADERS frame for stream ID 1 with length 14540 and flags END_STREAM, END_HEADERS"))); + + await host.StopAsync().DefaultTimeout(); + } + + // Settings_HeaderTableSize_CanBeReduced_Client - The client uses the default 4k HPACK dynamic table size and it cannot be changed. + // Nor does Kestrel yet support sending dynaimc table updates, so there's nothing to test here. https://github.com/aspnet/AspNetCore/issues/4715 + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_MaxConcurrentStreamsGet_Server(string scheme) + { + var sync = new SemaphoreSlim(5); + var requestCount = 0; + var requestBlock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureKestrel(options => + { + options.Limits.Http2.MaxStreamsPerConnection = 5; + }); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + // The stream limit should mean we never hit the semaphore limit. + Assert.True(sync.Wait(0)); + var count = Interlocked.Increment(ref requestCount); + + if (count == 5) + { + requestBlock.TrySetResult(0); + } + else + { + await requestBlock.Task.DefaultTimeout(); + } + + sync.Release(); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var tasks = new List>(10); + for (var i = 0; i < 10; i++) + { + var requestTask = client.GetAsync(url).DefaultTimeout(); + tasks.Add(requestTask); + } + + var responses = await Task.WhenAll(tasks.ToList()).DefaultTimeout(); + foreach (var response in responses) + { + response.EnsureSuccessStatusCode(); + } + + // SKIP: https://github.com/aspnet/AspNetCore/issues/17842 + // The client initially issues all 10 requests before receiving the settings, has 5 refused (after receiving the settings), + // waits for the first 5 to finish, retries the refused 5, and in the end each request completes sucesfully despite the logged errors. + // Assert.Empty(TestSink.Writes.Where(context => context.Message.Contains("HTTP/2 stream error"))); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory(Skip = "https://github.com/aspnet/AspNetCore/issues/17484")] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_MaxConcurrentStreamsPost_Server(string scheme) + { + var sync = new SemaphoreSlim(5); + var requestCount = 0; + var requestBlock = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureKestrel(options => + { + options.Limits.Http2.MaxStreamsPerConnection = 5; + }); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + // The stream limit should mean we never hit the semaphore limit. + Assert.True(sync.Wait(0)); + var count = Interlocked.Increment(ref requestCount); + + if (count == 5) + { + requestBlock.TrySetResult(0); + } + else + { + await requestBlock.Task.DefaultTimeout(); + } + + sync.Release(); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + + var tasks = new List>(10); + for (var i = 0; i < 10; i++) + { + var requestTask = client.PostAsync(url, new StringContent("Hello World")).DefaultTimeout(); + tasks.Add(requestTask); + } + + var responses = await Task.WhenAll(tasks.ToList()).DefaultTimeout(); + foreach (var response in responses) + { + response.EnsureSuccessStatusCode(); + } + + // SKIP: https://github.com/aspnet/AspNetCore/issues/17842 + // The client initially issues all 10 requests before receiving the settings, has 5 refused (after receiving the settings), + // waits for the first 5 to finish, retries the refused 5, and in the end each request completes sucesfully despite the logged errors. + // Assert.Empty(TestSink.Writes.Where(context => context.Message.Contains("HTTP/2 stream error"))); + + await host.StopAsync().DefaultTimeout(); + } + + // Settings_MaxConcurrentStreams_Client - Neither client or server support Push, nothing to test in this direction. + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_MaxFrameSize_Larger_Server(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureKestrel(options => options.Limits.Http2.MaxFrameSize = 1024 * 20); // The default is 16kb + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => context.Response.WriteAsync("Hello World"))); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + // Send an initial request to ensure the settings get synced before the real test. + var responseBody = await client.GetStringAsync(url).DefaultTimeout(); + Assert.Equal("Hello World", responseBody); + + var response = await client.PostAsync(url, new ByteArrayContent(new byte[1024 * 18])).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + + // SKIP: The client does not take advantage of a larger allowed frame size. + // https://github.com/dotnet/runtime/blob/48a78bfa13e9c710851690621fc2c0fe1637802c/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs#L483-L488 + // Assert.Single(TestSink.Writes.Where(context => context.Message.Contains("received DATA frame for stream ID 1 with length 18432 and flags NONE"))); + + await host.StopAsync().DefaultTimeout(); + } + + // Settings_MaxFrameSize_Larger_Client - Not configurable + + [Theory] + [Flaky("https://github.com/dotnet/runtime/issues/860", FlakyOn.All)] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_MaxHeaderListSize_Server(string scheme) + { + var oneKbString = new string('a', 1024); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => throw new NotImplementedException() )); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + // There's no point in waiting for the settings to sync, the client doesn't check the header list size setting. + // https://github.com/dotnet/runtime/blob/48a78bfa13e9c710851690621fc2c0fe1637802c/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs#L467-L494 + + var request = CreateRequestMessage(HttpMethod.Get, url, content: null); + // The default size limit is 32kb. + for (var i = 0; i < 33; i++) + { + request.Headers.Add("header" + i, oneKbString + i); + } + // Kestrel closes the connection rather than sending the recommended 431 response. https://github.com/aspnet/AspNetCore/issues/17861 + await Assert.ThrowsAsync(() => client.SendAsync(request)).DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_MaxHeaderListSize_Client(string scheme) + { + var oneKbString = new string('a', 1024); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + // The total header size limit is 64kb. + for (var i = 0; i < 65; i++) + { + context.Response.Headers.Append("header" + i, oneKbString + i); + } + return Task.CompletedTask; + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + + using var client = CreateClient(); + var ex = await Assert.ThrowsAsync(() => client.GetAsync(url)).DefaultTimeout(); + Assert.Equal("The HTTP response headers length exceeded the set limit of 65536 bytes.", ex.InnerException?.InnerException?.Message); + + await host.StopAsync().DefaultTimeout(); + } + + // Settings_InitialWindowSize_Lower_Server - Kestrel does not support lowering the InitialStreamWindowSize below the spec default 64kb. + // Settings_InitialWindowSize_Lower_Client - Not configurable. + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_InitialWindowSize_Server(string scheme) + { + var requestFinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + await requestFinished.Task.DefaultTimeout(); + + await context.Response.WriteAsync("Hello World"); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + + var streamingContent = new StreamingContent(); + var requestTask = client.PostAsync(url, streamingContent); + + // The spec default window is 64kb-1 and Kestrel's default is 96kb. + // We should be able to send the entire request body without getting blocked by flow control. + var oneKbString = new string('a', 1024); + for (var i = 0; i < 96; i++) + { + await streamingContent.SendAsync(oneKbString).DefaultTimeout(); + } + streamingContent.Complete(); + requestFinished.SetResult(0); + + var response = await requestTask.DefaultTimeout(); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task Settings_InitialWindowSize_Client(string scheme) + { + var responseFinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + // The spec default window is 64kb - 1. + // We should be able to send the entire response body without getting blocked by flow control. + var oneKbString = new string('a', 1024); + for (var i = 0; i < 63; i++) + { + await context.Response.WriteAsync(oneKbString).DefaultTimeout(); + } + await context.Response.WriteAsync(new string('a', 1023)).DefaultTimeout(); + await context.Response.CompleteAsync().DefaultTimeout(); + responseFinished.SetResult(0); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + + var requestTask = client.GetStreamAsync(url); + await responseFinished.Task.DefaultTimeout(); + var response = await requestTask.DefaultTimeout(); + + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task ConnectionWindowSize_Server(string scheme) + { + var requestFinished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(async context => + { + await requestFinished.Task.DefaultTimeout(); + var buffer = new byte[1024]; + var read = 0; + do + { + read = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length).DefaultTimeout(); + } while (read > 0); + + await context.Response.WriteAsync("Hello World"); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + + var streamingContent0 = new StreamingContent(); + var streamingContent1 = new StreamingContent(); + var requestTask0 = client.PostAsync(url, streamingContent0); + var requestTask1 = client.PostAsync(url, streamingContent1); + + // The spec default connection window is 64kb-1 and Kestrel's default is 128kb. + // We should be able to send two 64kb requests without getting blocked by flow control. + var oneKbString = new string('a', 1024); + for (var i = 0; i < 64; i++) + { + await streamingContent0.SendAsync(oneKbString).DefaultTimeout(); + await streamingContent1.SendAsync(oneKbString).DefaultTimeout(); + } + streamingContent0.Complete(); + streamingContent1.Complete(); + requestFinished.SetResult(0); + + var response0 = await requestTask0.DefaultTimeout(); + var response1 = await requestTask0.DefaultTimeout(); + response0.EnsureSuccessStatusCode(); + response1.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response0.Content.ReadAsStringAsync()); + Assert.Equal("Hello World", await response1.Content.ReadAsStringAsync()); + + await host.StopAsync().DefaultTimeout(); + } + + // ConnectionWindowSize_Client - impractical + // The spec default connection window is 64kb - 1 but the client default is 64Mb (not configurable). + // The client restricts streams to 64kb - 1 so we would need to issue 64 * 1024 requests to stress the connection window limit. + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task UnicodeRequestHost(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => + { + Assert.Equal("點.看", context.Request.Host.Host); + return context.Response.WriteAsync(context.Request.Host.Host); + })); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + var request = CreateRequestMessage(HttpMethod.Get, url, content: null); + request.Headers.Host = "xn--md7a.xn--c1y"; // Punycode + var response = await client.SendAsync(request).DefaultTimeout(); + response.EnsureSuccessStatusCode(); + Assert.Equal("點.看", await response.Content.ReadAsStringAsync()); + await host.StopAsync().DefaultTimeout(); + } + + [Theory] + [MemberData(nameof(SupportedSchemes))] + public async Task UrlEncoding(string scheme) + { + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + ConfigureKestrel(webHostBuilder, scheme); + webHostBuilder.ConfigureServices(AddTestLogging) + .Configure(app => app.Run(context => context.Response.WriteAsync(context.Request.Path.Value))); + }); + using var host = await hostBuilder.StartAsync().DefaultTimeout(); + + var url = host.MakeUrl(scheme); + using var client = CreateClient(); + // Skipped controls, '?' and '#'. + var response = await client.GetAsync(url + " !\"$%&'()*++,-./0123456789:;<>=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`{|}~點看").DefaultTimeout(); + response.EnsureSuccessStatusCode(); + Assert.Equal("/ !\"$%&'()*++,-./0123456789:;<>=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[/]^_`{|}~點看", await response.Content.ReadAsStringAsync()); + await host.StopAsync().DefaultTimeout(); } private static HttpClient CreateClient() @@ -552,8 +1631,16 @@ namespace Interop.FunctionalTests private static async Task ReadStreamHelloWorld(Stream stream) { var responseBuffer = new byte["Hello World".Length]; - var read = await stream.ReadAsync(responseBuffer, 0, responseBuffer.Length).DefaultTimeout(); - Assert.Equal(responseBuffer.Length, read); + var totalRead = 0; + do + { + var read = await stream.ReadAsync(responseBuffer, totalRead, responseBuffer.Length - totalRead).DefaultTimeout(); + totalRead += read; + if (read == 0) + { + throw new InvalidOperationException("Unexpected end of stream"); + } + } while (totalRead < responseBuffer.Length); Assert.Equal("Hello World", Encoding.UTF8.GetString(responseBuffer)); } }