diff --git a/src/Hosting/TestHost/src/ClientHandler.cs b/src/Hosting/TestHost/src/ClientHandler.cs index 580549322593..0b8cc69dc289 100644 --- a/src/Hosting/TestHost/src/ClientHandler.cs +++ b/src/Hosting/TestHost/src/ClientHandler.cs @@ -66,32 +66,35 @@ protected override async Task SendAsync( var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext); - var requestContent = request.Content ?? new StreamContent(Stream.Null); + var requestContent = request.Content; - // Read content from the request HttpContent into a pipe in a background task. This will allow the request - // delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios. - contextBuilder.SendRequestStream(async writer => + if (requestContent != null) { - if (requestContent is StreamContent) + // Read content from the request HttpContent into a pipe in a background task. This will allow the request + // delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios. + contextBuilder.SendRequestStream(async writer => { + if (requestContent is StreamContent) + { // This is odd but required for backwards compat. If StreamContent is passed in then seek to beginning. // This is safe because StreamContent.ReadAsStreamAsync doesn't block. It will return the inner stream. var body = await requestContent.ReadAsStreamAsync(); - if (body.CanSeek) - { + if (body.CanSeek) + { // This body may have been consumed before, rewind it. body.Seek(0, SeekOrigin.Begin); - } + } - await body.CopyToAsync(writer); - } - else - { - await requestContent.CopyToAsync(writer.AsStream()); - } + await body.CopyToAsync(writer); + } + else + { + await requestContent.CopyToAsync(writer.AsStream()); + } - await writer.CompleteAsync(); - }); + await writer.CompleteAsync(); + }); + } contextBuilder.Configure((context, reader) => { @@ -110,6 +113,39 @@ protected override async Task SendAsync( req.Scheme = request.RequestUri.Scheme; + var canHaveBody = false; + if (requestContent != null) + { + canHaveBody = true; + // Chunked takes precedence over Content-Length, don't create a request with both Content-Length and chunked. + if (request.Headers.TransferEncodingChunked != true) + { + // Reading the ContentLength will add it to the Headers‼ + // https://github.com/dotnet/runtime/blob/874399ab15e47c2b4b7c6533cc37d27d47cb5242/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpContentHeaders.cs#L68-L87 + var contentLength = requestContent.Headers.ContentLength; + if (!contentLength.HasValue && request.Version == HttpVersion.Version11) + { + // HTTP/1.1 requests with a body require either Content-Length or Transfer-Encoding: chunked. + request.Headers.TransferEncodingChunked = true; + } + else if (contentLength == 0) + { + canHaveBody = false; + } + } + + foreach (var header in requestContent.Headers) + { + req.Headers.Append(header.Key, header.Value.ToArray()); + } + + if (canHaveBody) + { + req.Body = new AsyncStreamWrapper(reader.AsStream(), () => contextBuilder.AllowSynchronousIO); + } + } + context.Features.Set(new RequestBodyDetectionFeature(canHaveBody)); + foreach (var header in request.Headers) { // User-Agent is a space delineated single line header but HttpRequestHeaders parses it as multiple elements. @@ -141,17 +177,6 @@ protected override async Task SendAsync( req.PathBase = _pathBase; } req.QueryString = QueryString.FromUriComponent(request.RequestUri); - - // Reading the ContentLength will add it to the Headers‼ - // https://github.com/dotnet/runtime/blob/874399ab15e47c2b4b7c6533cc37d27d47cb5242/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpContentHeaders.cs#L68-L87 - _ = requestContent.Headers.ContentLength; - - foreach (var header in requestContent.Headers) - { - req.Headers.Append(header.Key, header.Value.ToArray()); - } - - req.Body = new AsyncStreamWrapper(reader.AsStream(), () => contextBuilder.AllowSynchronousIO); }); var response = new HttpResponseMessage(); diff --git a/src/Hosting/TestHost/src/RequestBodyDetectionFeature.cs b/src/Hosting/TestHost/src/RequestBodyDetectionFeature.cs new file mode 100644 index 000000000000..1b9b019073c9 --- /dev/null +++ b/src/Hosting/TestHost/src/RequestBodyDetectionFeature.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.TestHost +{ + internal class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature + { + public RequestBodyDetectionFeature(bool canHaveBody) + { + CanHaveBody = canHaveBody; + } + + public bool CanHaveBody { get; } + } +} diff --git a/src/Hosting/TestHost/test/ClientHandlerTests.cs b/src/Hosting/TestHost/test/ClientHandlerTests.cs index 3e3406e6f5a4..bd67c9b5d54e 100644 --- a/src/Hosting/TestHost/test/ClientHandlerTests.cs +++ b/src/Hosting/TestHost/test/ClientHandlerTests.cs @@ -111,7 +111,9 @@ public Task ContentLengthWithBodyWorks() var contentBytes = Encoding.UTF8.GetBytes("This is a content!"); var handler = new ClientHandler(new PathString(""), new DummyApplication(context => { + Assert.True(context.Request.CanHaveBody()); Assert.Equal(contentBytes.LongLength, context.Request.ContentLength); + Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); return Task.CompletedTask; })); @@ -122,11 +124,13 @@ public Task ContentLengthWithBodyWorks() } [Fact] - public Task ContentLengthWithNoBodyWorks() + public Task ContentLengthNotPresentWithNoBody() { var handler = new ClientHandler(new PathString(""), new DummyApplication(context => { - Assert.Equal(0, context.Request.ContentLength); + Assert.False(context.Request.CanHaveBody()); + Assert.Null(context.Request.ContentLength); + Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); return Task.CompletedTask; })); @@ -136,11 +140,13 @@ public Task ContentLengthWithNoBodyWorks() } [Fact] - public Task ContentLengthWithChunkedTransferEncodingWorks() + public Task ContentLengthWithImplicitChunkedTransferEncodingWorks() { var handler = new ClientHandler(new PathString(""), new DummyApplication(context => { + Assert.True(context.Request.CanHaveBody()); Assert.Null(context.Request.ContentLength); + Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]); return Task.CompletedTask; })); @@ -150,6 +156,26 @@ public Task ContentLengthWithChunkedTransferEncodingWorks() return httpClient.PostAsync("http://example.com", new UnlimitedContent()); } + [Fact] + public Task ContentLengthWithExplicitChunkedTransferEncodingWorks() + { + var handler = new ClientHandler(new PathString(""), new DummyApplication(context => + { + Assert.True(context.Request.CanHaveBody()); + Assert.Null(context.Request.ContentLength); + Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]); + + return Task.CompletedTask; + })); + + var httpClient = new HttpClient(handler); + httpClient.DefaultRequestHeaders.TransferEncodingChunked = true; + var contentBytes = Encoding.UTF8.GetBytes("This is a content!"); + var content = new ByteArrayContent(contentBytes); + + return httpClient.PostAsync("http://example.com", content); + } + [Fact] public async Task ServerTrailersSetOnResponseAfterContentRead() { diff --git a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs index d1e740dafbd9..dee9167048cb 100644 --- a/src/Hosting/TestHost/test/HttpContextBuilderTests.cs +++ b/src/Hosting/TestHost/test/HttpContextBuilderTests.cs @@ -40,6 +40,7 @@ public async Task ExpectedValuesAreAvailable() Assert.Equal("/A/Path", context.Request.PathBase.Value); Assert.Equal("/and/file.txt", context.Request.Path.Value); Assert.Equal("?and=query", context.Request.QueryString.Value); + Assert.Null(context.Request.CanHaveBody()); Assert.NotNull(context.Request.Body); Assert.NotNull(context.Request.Headers); Assert.NotNull(context.Response.Headers); diff --git a/src/Hosting/TestHost/test/TestServerTests.cs b/src/Hosting/TestHost/test/TestServerTests.cs index 0ffaf8b8b4fe..395fdf5752df 100644 --- a/src/Hosting/TestHost/test/TestServerTests.cs +++ b/src/Hosting/TestHost/test/TestServerTests.cs @@ -228,6 +228,7 @@ public async Task DispoingTheRequestBodyDoesNotDisposeClientStreams() var stream = new ThrowOnDisposeStream(); stream.Write(Encoding.ASCII.GetBytes("Hello World")); + stream.Seek(0, SeekOrigin.Begin); var response = await server.CreateClient().PostAsync("/", new StreamContent(stream)); Assert.True(response.IsSuccessStatusCode); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); diff --git a/src/Hosting/TestHost/test/Utilities.cs b/src/Hosting/TestHost/test/Utilities.cs index da5d433f4c2a..d0fbbf65be55 100644 --- a/src/Hosting/TestHost/test/Utilities.cs +++ b/src/Hosting/TestHost/test/Utilities.cs @@ -3,6 +3,8 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing; namespace Microsoft.AspNetCore.TestHost @@ -14,5 +16,10 @@ internal static class Utilities internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout); internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout); + + internal static bool? CanHaveBody(this HttpRequest request) + { + return request.HttpContext.Features.Get()?.CanHaveBody; + } } } diff --git a/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs b/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs new file mode 100644 index 000000000000..c44ef1074a9a --- /dev/null +++ b/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs @@ -0,0 +1,29 @@ +// 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. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Used to indicate if the request can have a body. + /// + public interface IHttpRequestBodyDetectionFeature + { + /// + /// Indicates if the request can have a body. + /// + /// + /// This returns true when: + /// - It's an HTTP/1.x request with a non-zero Content-Length or a 'Transfer-Encoding: chunked' header. + /// - It's an HTTP/2 request that did not set the END_STREAM flag on the initial headers frame. + /// The final request body length may still be zero for the chunked or HTTP/2 scenarios. + /// + /// This returns false when: + /// - It's an HTTP/1.x request with no Content-Length or 'Transfer-Encoding: chunked' header, or the Content-Length is 0. + /// - It's an HTTP/1.x request with Connection: Upgrade (e.g. WebSockets). There is no HTTP request body for these requests and + /// no data should be received until after the upgrade. + /// - It's an HTTP/2 request that set END_STREAM on the initial headers frame. + /// When false, the request body should never return data. + /// + bool CanHaveBody { get; } + } +} diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs index 3d505c157871..caba8dbcbe4a 100644 --- a/src/Servers/HttpSys/src/FeatureContext.cs +++ b/src/Servers/HttpSys/src/FeatureContext.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys { internal class FeatureContext : IHttpRequestFeature, + IHttpRequestBodyDetectionFeature, IHttpConnectionFeature, IHttpResponseFeature, IHttpResponseBodyFeature, @@ -212,6 +213,8 @@ string IHttpRequestFeature.Scheme set { _scheme = value; } } + bool IHttpRequestBodyDetectionFeature.CanHaveBody => Request.HasEntityBody; + IPAddress IHttpConnectionFeature.LocalIpAddress { get diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs index 494f3e5d6deb..2c9e942d941b 100644 --- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs +++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs @@ -135,6 +135,7 @@ public long? ContentLength { if (_contentBoundaryType == BoundaryType.None) { + // Note Http.Sys adds the Transfer-Encoding: chunked header to HTTP/2 requests with bodies for back compat. string transferEncoding = Headers[HttpKnownHeaderNames.TransferEncoding]; if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Servers/HttpSys/src/StandardFeatureCollection.cs b/src/Servers/HttpSys/src/StandardFeatureCollection.cs index c8705b2ee6d2..7bc7802c28c5 100644 --- a/src/Servers/HttpSys/src/StandardFeatureCollection.cs +++ b/src/Servers/HttpSys/src/StandardFeatureCollection.cs @@ -17,6 +17,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection private static readonly Dictionary> _featureFuncLookup = new Dictionary>() { { typeof(IHttpRequestFeature), _identityFunc }, + { typeof(IHttpRequestBodyDetectionFeature), _identityFunc }, { typeof(IHttpConnectionFeature), _identityFunc }, { typeof(IHttpResponseFeature), _identityFunc }, { typeof(IHttpResponseBodyFeature), _identityFunc }, diff --git a/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs b/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs index 38c76dc1ab6b..740cec75b699 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Http2Tests.cs @@ -2,9 +2,12 @@ // 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; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -31,6 +34,7 @@ public async Task EmptyResponse_200() using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => { // Default 200 + Assert.False(httpContext.Request.CanHaveBody()); return Task.CompletedTask; }); @@ -64,6 +68,233 @@ await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => .Build().RunAsync(); } + [ConditionalTheory] + [InlineData("POST")] + [InlineData("PUT")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task RequestWithoutData_LengthRequired_Rejected(string method) + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + throw new NotImplementedException(); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await h2Connection.StartStreamAsync(1, headers, endStream: true); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("411", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 344); + dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalTheory] + [InlineData("GET")] + [InlineData("HEAD")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task RequestWithoutData_Success(string method) + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + Assert.True(HttpMethods.Equals(method, httpContext.Request.Method)); + Assert.False(httpContext.Request.CanHaveBody()); + Assert.Null(httpContext.Request.ContentLength); + Assert.False(httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); + return Task.CompletedTask; + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await h2Connection.StartStreamAsync(1, headers, endStream: true); + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + if (Environment.OSVersion.Version >= Win10_Regressed_DataFrame) + { + // TODO: Remove when the regression is fixed. + // https://github.com/dotnet/aspnetcore/issues/23164#issuecomment-652646163 + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 0); + + dataFrame = await h2Connection.ReceiveFrameAsync(); + } + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalTheory] + [InlineData("GET")] + // [InlineData("HEAD")] Reset with code HTTP_1_1_REQUIRED + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task RequestWithDataAndContentLength_Success(string method) + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + Assert.True(HttpMethods.Equals(method, httpContext.Request.Method)); + Assert.True(httpContext.Request.CanHaveBody()); + Assert.Equal(11, httpContext.Request.ContentLength); + Assert.False(httpContext.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); + return httpContext.Request.Body.CopyToAsync(httpContext.Response.Body); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair(HeaderNames.ContentLength, "11"), + }; + + await h2Connection.StartStreamAsync(1, headers, endStream: false); + + await h2Connection.SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true); + + // Http.Sys no longer sends a window update here on later versions. + if (Environment.OSVersion.Version < new Version(10, 0, 19041, 0)) + { + var windowUpdate = await h2Connection.ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.WINDOW_UPDATE, windowUpdate.Type); + } + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 11); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); + + dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + + [ConditionalTheory] + [InlineData("GET")] + // [InlineData("HEAD")] Reset with code HTTP_1_1_REQUIRED + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] + public async Task RequestWithDataAndNoContentLength_Success(string method) + { + using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => + { + Assert.True(HttpMethods.Equals(method, httpContext.Request.Method)); + Assert.True(httpContext.Request.CanHaveBody()); + Assert.Null(httpContext.Request.ContentLength); + // The client didn't send this header, Http.Sys added it for back compat with HTTP/1.1. + Assert.Equal("chunked", httpContext.Request.Headers[HeaderNames.TransferEncoding]); + return httpContext.Request.Body.CopyToAsync(httpContext.Response.Body); + }); + + await new HostBuilder() + .UseHttp2Cat(address, async h2Connection => + { + await h2Connection.InitializeConnectionAsync(); + + h2Connection.Logger.LogInformation("Initialized http2 connection. Starting stream 1."); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "https"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + + await h2Connection.StartStreamAsync(1, headers, endStream: false); + + await h2Connection.SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true); + + // Http.Sys no longer sends a window update here on later versions. + if (Environment.OSVersion.Version < new Version(10, 0, 19041, 0)) + { + var windowUpdate = await h2Connection.ReceiveFrameAsync(); + Assert.Equal(Http2FrameType.WINDOW_UPDATE, windowUpdate.Type); + } + + await h2Connection.ReceiveHeadersAsync(1, decodedHeaders => + { + Assert.Equal("200", decodedHeaders[HeaderNames.Status]); + }); + + var dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: false, length: 11); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); + + dataFrame = await h2Connection.ReceiveFrameAsync(); + Http2Utilities.VerifyDataFrame(dataFrame, 1, endOfStream: true, length: 0); + + h2Connection.Logger.LogInformation("Connection stopped."); + }) + .Build().RunAsync(); + } + [ConditionalFact] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] public async Task ResponseWithData_Success() diff --git a/src/Servers/HttpSys/test/FunctionalTests/RequestBodyLimitTests.cs b/src/Servers/HttpSys/test/FunctionalTests/RequestBodyLimitTests.cs index 4f825d8ae654..d9c26166e64c 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/RequestBodyLimitTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/RequestBodyLimitTests.cs @@ -48,6 +48,7 @@ public async Task ContentLengthEqualsLimit_ReadAsync_Success() var feature = httpContext.Features.Get(); Assert.NotNull(feature); Assert.False(feature.IsReadOnly); + Assert.True(httpContext.Request.CanHaveBody()); Assert.Equal(11, httpContext.Request.ContentLength); byte[] input = new byte[100]; int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); @@ -114,6 +115,7 @@ public async Task ChunkedEqualsLimit_ReadAsync_Success() var feature = httpContext.Features.Get(); Assert.NotNull(feature); Assert.False(feature.IsReadOnly); + Assert.True(httpContext.Request.CanHaveBody()); Assert.Null(httpContext.Request.ContentLength); byte[] input = new byte[100]; int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); diff --git a/src/Servers/HttpSys/test/FunctionalTests/RequestBodyTests.cs b/src/Servers/HttpSys/test/FunctionalTests/RequestBodyTests.cs index 9c67acf6c693..232ccbbe528b 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/RequestBodyTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/RequestBodyTests.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing; using Xunit; @@ -23,6 +24,7 @@ public async Task RequestBody_ReadSync_Success() string address; using (Utilities.CreateHttpServer(out address, httpContext => { + Assert.True(httpContext.Request.CanHaveBody()); byte[] input = new byte[100]; httpContext.Features.Get().AllowSynchronousIO = true; int read = httpContext.Request.Body.Read(input, 0, input.Length); @@ -42,6 +44,7 @@ public async Task RequestBody_ReadAsync_Success() string address; using (Utilities.CreateHttpServer(out address, async httpContext => { + Assert.True(httpContext.Request.CanHaveBody()); byte[] input = new byte[100]; int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); httpContext.Response.ContentLength = read; diff --git a/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs b/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs index 4a62beb07e30..da4700b46751 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/RequestTests.cs @@ -44,6 +44,7 @@ public async Task Request_SimpleGet_ExpectedFieldsSet() Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget); Assert.Equal("HTTP/1.1", requestInfo.Protocol); + Assert.False(httpContext.Request.CanHaveBody()); var connectionInfo = httpContext.Features.Get(); Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString()); Assert.NotEqual(0, connectionInfo.RemotePort); diff --git a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs index 4aa56ccabfe5..1b94bc511184 100644 --- a/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs +++ b/src/Servers/HttpSys/test/FunctionalTests/Utilities.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -170,5 +171,10 @@ internal static IServer CreateDynamicHttpsServer(string basePath, out string roo internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout); internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout); + + internal static bool? CanHaveBody(this HttpRequest request) + { + return request.HttpContext.Features.Get()?.CanHaveBody; + } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs index 1f2e1adfa294..e829c336742e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal partial class HttpProtocol : IHttpRequestFeature, + IHttpRequestBodyDetectionFeature, IHttpResponseFeature, IHttpResponseBodyFeature, IRequestBodyPipeFeature, @@ -121,6 +122,8 @@ PipeReader IRequestBodyPipeFeature.Reader } } + bool IHttpRequestBodyDetectionFeature.CanHaveBody => _bodyControl.CanHaveBody; + bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable; IHeaderDictionary IHttpRequestTrailersFeature.Trailers diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs index f6833a47486c..52c0308f89a1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http internal partial class HttpProtocol : IFeatureCollection { private object _currentIHttpRequestFeature; + private object _currentIHttpRequestBodyDetectionFeature; private object _currentIHttpResponseFeature; private object _currentIHttpResponseBodyFeature; private object _currentIRequestBodyPipeFeature; @@ -48,6 +49,7 @@ internal partial class HttpProtocol : IFeatureCollection private void FastReset() { _currentIHttpRequestFeature = this; + _currentIHttpRequestBodyDetectionFeature = this; _currentIHttpResponseFeature = this; _currentIHttpResponseBodyFeature = this; _currentIRequestBodyPipeFeature = this; @@ -133,6 +135,10 @@ object IFeatureCollection.this[Type key] { feature = _currentIHttpRequestFeature; } + else if (key == typeof(IHttpRequestBodyDetectionFeature)) + { + feature = _currentIHttpRequestBodyDetectionFeature; + } else if (key == typeof(IHttpResponseFeature)) { feature = _currentIHttpResponseFeature; @@ -253,6 +259,10 @@ object IFeatureCollection.this[Type key] { _currentIHttpRequestFeature = value; } + else if (key == typeof(IHttpRequestBodyDetectionFeature)) + { + _currentIHttpRequestBodyDetectionFeature = value; + } else if (key == typeof(IHttpResponseFeature)) { _currentIHttpResponseFeature = value; @@ -371,6 +381,10 @@ TFeature IFeatureCollection.Get() { feature = (TFeature)_currentIHttpRequestFeature; } + else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature)) + { + feature = (TFeature)_currentIHttpRequestBodyDetectionFeature; + } else if (typeof(TFeature) == typeof(IHttpResponseFeature)) { feature = (TFeature)_currentIHttpResponseFeature; @@ -495,6 +509,10 @@ void IFeatureCollection.Set(TFeature feature) { _currentIHttpRequestFeature = feature; } + else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature)) + { + _currentIHttpRequestBodyDetectionFeature = feature; + } else if (typeof(TFeature) == typeof(IHttpResponseFeature)) { _currentIHttpResponseFeature = feature; @@ -611,6 +629,10 @@ private IEnumerable> FastEnumerable() { yield return new KeyValuePair(typeof(IHttpRequestFeature), _currentIHttpRequestFeature); } + if (_currentIHttpRequestBodyDetectionFeature != null) + { + yield return new KeyValuePair(typeof(IHttpRequestBodyDetectionFeature), _currentIHttpRequestBodyDetectionFeature); + } if (_currentIHttpResponseFeature != null) { yield return new KeyValuePair(typeof(IHttpResponseFeature), _currentIHttpResponseFeature); diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/BodyControl.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/BodyControl.cs index 5a19dfad32a2..d7c6d1bdd1ee 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/BodyControl.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/BodyControl.cs @@ -36,6 +36,8 @@ public BodyControl(IHttpBodyControlFeature bodyControl, IHttpResponseControl res _upgradeStream = new HttpUpgradeStream(_request, _response); } + public bool CanHaveBody { get; private set; } + public Stream Upgrade() { // causes writes to context.Response.Body to throw @@ -46,6 +48,7 @@ public Stream Upgrade() public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body) { + CanHaveBody = !body.IsEmpty; _requestReader.StartAcceptingReads(body); _emptyRequestReader.StartAcceptingReads(MessageBody.ZeroContentLengthClose); _responseWriter.StartAcceptingWrites(); diff --git a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs index f62bad9117e8..c13f89253790 100644 --- a/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpProtocolFeatureCollectionTests.cs @@ -116,6 +116,7 @@ public void FeaturesByGenericSameAsByType() public void FeaturesSetByTypeSameAsGeneric() { _collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection(); + _collection[typeof(IHttpRequestBodyDetectionFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpResponseBodyFeature)] = CreateHttp1Connection(); _collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection(); @@ -139,6 +140,7 @@ public void FeaturesSetByTypeSameAsGeneric() public void FeaturesSetByGenericSameAsByType() { _collection.Set(CreateHttp1Connection()); + _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); _collection.Set(CreateHttp1Connection()); @@ -194,6 +196,7 @@ public void Http2StreamFeatureCollectionDoesIncludeIHttpMinRequestBodyDataRateFe private void CompareGenericGetterToIndexer() { Assert.Same(_collection.Get(), _collection[typeof(IHttpRequestFeature)]); + Assert.Same(_collection.Get(), _collection[typeof(IHttpRequestBodyDetectionFeature)]); Assert.Same(_collection.Get(), _collection[typeof(IHttpResponseFeature)]); Assert.Same(_collection.Get(), _collection[typeof(IHttpResponseBodyFeature)]); Assert.Same(_collection.Get(), _collection[typeof(IRequestBodyPipeFeature)]); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs index 2ec68a9c1808..a955b46c5630 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/ChunkedRequestTests.cs @@ -24,6 +24,7 @@ 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]; @@ -40,6 +41,7 @@ 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(); @@ -58,6 +60,7 @@ 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(); @@ -174,6 +177,7 @@ public async Task RequestBodyIsConsumedAutomaticallyIfAppDoesntConsumeItFully() { var response = httpContext.Response; var request = httpContext.Request; + Assert.True(request.CanHaveBody()); Assert.Equal("POST", request.Method); @@ -229,6 +233,7 @@ public async Task TrailingHeadersAreParsed() { var response = httpContext.Response; var request = httpContext.Request; + Assert.True(request.CanHaveBody()); var buffer = new byte[200]; @@ -356,6 +361,7 @@ public async Task TrailingHeadersAreParsedWithPipe() { var response = httpContext.Response; var request = httpContext.Request; + Assert.True(request.CanHaveBody()); // The first request is chunked with no trailers. if (requestsReceived == 0) @@ -652,6 +658,7 @@ public async Task InvalidLengthResultsIn400() { var response = httpContext.Response; var request = httpContext.Request; + Assert.True(request.CanHaveBody()); var buffer = new byte[200]; @@ -695,6 +702,7 @@ public async Task InvalidSizedDataResultsIn400() { var response = httpContext.Response; var request = httpContext.Request; + Assert.True(request.CanHaveBody()); var buffer = new byte[200]; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs index 3ca2b86c14b0..19ed8d852ba6 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs @@ -64,22 +64,28 @@ public async Task HEADERS_Received_InvalidCustomMethod_Reset() await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); } - [Fact] - public async Task HEADERS_Received_CustomMethod_Accepted() + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + public async Task HEADERS_Received_KnownOrCustomMethods_Accepted(string method) { var headers = new[] { - new KeyValuePair(HeaderNames.Method, "Custom"), + new KeyValuePair(HeaderNames.Method, method), new KeyValuePair(HeaderNames.Path, "/"), new KeyValuePair(HeaderNames.Scheme, "http"), new KeyValuePair(HeaderNames.Authority, "localhost:80"), }; - await InitializeConnectionAsync(_echoMethod); + await InitializeConnectionAsync(_echoMethodNoBody); await StartStreamAsync(1, headers, endStream: true); var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, - withLength: 51, + withLength: 45 + method.Length, withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withStreamId: 1); @@ -90,14 +96,146 @@ public async Task HEADERS_Received_CustomMethod_Accepted() Assert.Equal(4, _decodedHeaders.Count); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); - Assert.Equal("Custom", _decodedHeaders["Method"]); - Assert.Equal("0", _decodedHeaders["content-length"]); + Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]); + Assert.Equal(method, _decodedHeaders["Method"]); + } + + [Fact] + public async Task HEADERS_Received_HEADMethod_Accepted() + { + await InitializeConnectionAsync(_echoMethodNoBody); + + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, "HEAD"), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 45, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + + Assert.Equal(3, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("HEAD", _decodedHeaders["Method"]); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + public async Task HEADERS_Received_MethodsWithContentLength_Accepted(string method) + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + new KeyValuePair(HeaderNames.ContentLength, "11"), + }; + await InitializeConnectionAsync(context => + { + Assert.True(HttpMethods.Equals(method, context.Request.Method)); + Assert.True(context.Request.CanHaveBody()); + Assert.Equal(11, context.Request.ContentLength); + Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); + return context.Request.BodyReader.CopyToAsync(context.Response.BodyWriter); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 1); + var dataFrame = await ExpectAsync(Http2FrameType.DATA, + withLength: 11, + withFlags: (byte)(Http2HeadersFrameFlags.NONE), + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("CUSTOM")] + public async Task HEADERS_Received_MethodsWithoutContentLength_Accepted(string method) + { + var headers = new[] + { + new KeyValuePair(HeaderNames.Method, method), + new KeyValuePair(HeaderNames.Path, "/"), + new KeyValuePair(HeaderNames.Scheme, "http"), + new KeyValuePair(HeaderNames.Authority, "localhost:80"), + }; + await InitializeConnectionAsync(context => + { + Assert.True(HttpMethods.Equals(method, context.Request.Method)); + Assert.True(context.Request.CanHaveBody()); + Assert.Null(context.Request.ContentLength); + Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding)); + return context.Request.BodyReader.CopyToAsync(context.Response.BodyWriter); + }); + + await StartStreamAsync(1, headers, endStream: false); + await SendDataAsync(1, Encoding.UTF8.GetBytes("Hello World"), endStream: true); + + var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, + withLength: 32, + withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS), + withStreamId: 1); + var dataFrame = await ExpectAsync(Http2FrameType.DATA, + withLength: 11, + withFlags: (byte)(Http2HeadersFrameFlags.NONE), + withStreamId: 1); + await ExpectAsync(Http2FrameType.DATA, + withLength: 0, + withFlags: (byte)(Http2HeadersFrameFlags.END_STREAM), + withStreamId: 1); + + await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); + + _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this); + + Assert.Equal(2, _decodedHeaders.Count); + Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); + Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); + Assert.Equal("Hello World", Encoding.UTF8.GetString(dataFrame.Payload.Span)); } [Fact] public async Task HEADERS_Received_CONNECTMethod_Accepted() { - await InitializeConnectionAsync(_echoMethod); + await InitializeConnectionAsync(_echoMethodNoBody); // :path and :scheme are not allowed, :authority is optional var headers = new[] { new KeyValuePair(HeaderNames.Method, "CONNECT") }; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 7714d1e342e5..1807062502be 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -150,7 +150,7 @@ protected static IEnumerable> ReadRateRequestHeader protected readonly RequestDelegate _waitForAbortApplication; protected readonly RequestDelegate _waitForAbortFlushingApplication; protected readonly RequestDelegate _readRateApplication; - protected readonly RequestDelegate _echoMethod; + protected readonly RequestDelegate _echoMethodNoBody; protected readonly RequestDelegate _echoHost; protected readonly RequestDelegate _echoPath; protected readonly RequestDelegate _appAbort; @@ -346,8 +346,9 @@ public Http2TestBase() await stalledReadTask; }; - _echoMethod = context => + _echoMethodNoBody = context => { + Assert.False(context.Request.CanHaveBody()); context.Response.Headers["Method"] = context.Request.Method; return Task.CompletedTask; diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs index cd8c95f78b71..369b8a100f74 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/MaxRequestBodySizeTests.cs @@ -28,6 +28,7 @@ public async Task RejectsRequestWithContentLengthHeaderExceedingGlobalLimit() await using (var server = new TestServer(async context => { + Assert.True(context.Request.CanHaveBody()); var buffer = new byte[1]; #pragma warning disable CS0618 // Type or member is obsolete requestRejectedEx = await Assert.ThrowsAsync( diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs index 873b4c9ec772..dd32baf2bbfc 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs @@ -1933,7 +1933,7 @@ public async Task ContentLengthCallCompleteWithExceptionCauses500() var request = httpContext.Request; Assert.Equal("POST", request.Method); - + Assert.True(request.CanHaveBody()); var readResult = await request.BodyReader.ReadAsync(); request.BodyReader.AdvanceTo(readResult.Buffer.End); diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/RequestExtensions.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/RequestExtensions.cs new file mode 100644 index 000000000000..6093563f6703 --- /dev/null +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/TestTransport/RequestExtensions.cs @@ -0,0 +1,15 @@ +// 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 Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http +{ + internal static class RequestExtensions + { + internal static bool? CanHaveBody(this HttpRequest request) + { + return request.HttpContext.Features.Get()?.CanHaveBody; + } + } +} diff --git a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs index f7e77626c70b..7c8dc1bce725 100644 --- a/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs +++ b/src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs @@ -12,6 +12,7 @@ public static string GenerateFile() var alwaysFeatures = new[] { "IHttpRequestFeature", + "IHttpRequestBodyDetectionFeature", "IHttpResponseFeature", "IHttpResponseBodyFeature", "IRequestBodyPipeFeature", @@ -58,6 +59,7 @@ public static string GenerateFile() var implementedFeatures = new[] { "IHttpRequestFeature", + "IHttpRequestBodyDetectionFeature", "IHttpResponseFeature", "IHttpResponseBodyFeature", "IRequestBodyPipeFeature",