Identifying if a request has a body #24175 (#24984)

This commit is contained in:
Chris Ross 2020-08-23 23:38:40 -07:00 committed by GitHub
parent df37d00307
commit 11bae8a112
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 591 additions and 41 deletions

View File

@ -66,32 +66,35 @@ namespace Microsoft.AspNetCore.TestHost
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext); 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 if (requestContent != null)
// delegate to start before the request HttpContent is complete. A background task allows duplex streaming scenarios.
contextBuilder.SendRequestStream(async writer =>
{ {
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 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. // This is safe because StreamContent.ReadAsStreamAsync doesn't block. It will return the inner stream.
var body = await requestContent.ReadAsStreamAsync(); var body = await requestContent.ReadAsStreamAsync();
if (body.CanSeek) if (body.CanSeek)
{ {
// This body may have been consumed before, rewind it. // This body may have been consumed before, rewind it.
body.Seek(0, SeekOrigin.Begin); body.Seek(0, SeekOrigin.Begin);
}
await body.CopyToAsync(writer);
}
else
{
await requestContent.CopyToAsync(writer.AsStream());
} }
await body.CopyToAsync(writer); await writer.CompleteAsync();
} });
else }
{
await requestContent.CopyToAsync(writer.AsStream());
}
await writer.CompleteAsync();
});
contextBuilder.Configure((context, reader) => contextBuilder.Configure((context, reader) =>
{ {
@ -110,6 +113,39 @@ namespace Microsoft.AspNetCore.TestHost
req.Scheme = request.RequestUri.Scheme; 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<IHttpRequestBodyDetectionFeature>(new RequestBodyDetectionFeature(canHaveBody));
foreach (var header in request.Headers) foreach (var header in request.Headers)
{ {
// User-Agent is a space delineated single line header but HttpRequestHeaders parses it as multiple elements. // User-Agent is a space delineated single line header but HttpRequestHeaders parses it as multiple elements.
@ -141,17 +177,6 @@ namespace Microsoft.AspNetCore.TestHost
req.PathBase = _pathBase; req.PathBase = _pathBase;
} }
req.QueryString = QueryString.FromUriComponent(request.RequestUri); 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(); var response = new HttpResponseMessage();

View File

@ -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; }
}
}

View File

@ -111,7 +111,9 @@ namespace Microsoft.AspNetCore.TestHost
var contentBytes = Encoding.UTF8.GetBytes("This is a content!"); var contentBytes = Encoding.UTF8.GetBytes("This is a content!");
var handler = new ClientHandler(new PathString(""), new DummyApplication(context => var handler = new ClientHandler(new PathString(""), new DummyApplication(context =>
{ {
Assert.True(context.Request.CanHaveBody());
Assert.Equal(contentBytes.LongLength, context.Request.ContentLength); Assert.Equal(contentBytes.LongLength, context.Request.ContentLength);
Assert.False(context.Request.Headers.ContainsKey(HeaderNames.TransferEncoding));
return Task.CompletedTask; return Task.CompletedTask;
})); }));
@ -122,11 +124,13 @@ namespace Microsoft.AspNetCore.TestHost
} }
[Fact] [Fact]
public Task ContentLengthWithNoBodyWorks() public Task ContentLengthNotPresentWithNoBody()
{ {
var handler = new ClientHandler(new PathString(""), new DummyApplication(context => 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; return Task.CompletedTask;
})); }));
@ -136,11 +140,13 @@ namespace Microsoft.AspNetCore.TestHost
} }
[Fact] [Fact]
public Task ContentLengthWithChunkedTransferEncodingWorks() public Task ContentLengthWithImplicitChunkedTransferEncodingWorks()
{ {
var handler = new ClientHandler(new PathString(""), new DummyApplication(context => var handler = new ClientHandler(new PathString(""), new DummyApplication(context =>
{ {
Assert.True(context.Request.CanHaveBody());
Assert.Null(context.Request.ContentLength); Assert.Null(context.Request.ContentLength);
Assert.Equal("chunked", context.Request.Headers[HeaderNames.TransferEncoding]);
return Task.CompletedTask; return Task.CompletedTask;
})); }));
@ -150,6 +156,26 @@ namespace Microsoft.AspNetCore.TestHost
return httpClient.PostAsync("http://example.com", new UnlimitedContent()); 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] [Fact]
public async Task ServerTrailersSetOnResponseAfterContentRead() public async Task ServerTrailersSetOnResponseAfterContentRead()
{ {

View File

@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.Equal("/A/Path", context.Request.PathBase.Value); Assert.Equal("/A/Path", context.Request.PathBase.Value);
Assert.Equal("/and/file.txt", context.Request.Path.Value); Assert.Equal("/and/file.txt", context.Request.Path.Value);
Assert.Equal("?and=query", context.Request.QueryString.Value); Assert.Equal("?and=query", context.Request.QueryString.Value);
Assert.Null(context.Request.CanHaveBody());
Assert.NotNull(context.Request.Body); Assert.NotNull(context.Request.Body);
Assert.NotNull(context.Request.Headers); Assert.NotNull(context.Request.Headers);
Assert.NotNull(context.Response.Headers); Assert.NotNull(context.Response.Headers);

View File

@ -228,6 +228,7 @@ namespace Microsoft.AspNetCore.TestHost
var stream = new ThrowOnDisposeStream(); var stream = new ThrowOnDisposeStream();
stream.Write(Encoding.ASCII.GetBytes("Hello World")); stream.Write(Encoding.ASCII.GetBytes("Hello World"));
stream.Seek(0, SeekOrigin.Begin);
var response = await server.CreateClient().PostAsync("/", new StreamContent(stream)); var response = await server.CreateClient().PostAsync("/", new StreamContent(stream));
Assert.True(response.IsSuccessStatusCode); Assert.True(response.IsSuccessStatusCode);
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());

View File

@ -3,6 +3,8 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
namespace Microsoft.AspNetCore.TestHost namespace Microsoft.AspNetCore.TestHost
@ -14,5 +16,10 @@ namespace Microsoft.AspNetCore.TestHost
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout); internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
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<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
}
} }
} }

View File

@ -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
{
/// <summary>
/// Used to indicate if the request can have a body.
/// </summary>
public interface IHttpRequestBodyDetectionFeature
{
/// <summary>
/// Indicates if the request can have a body.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
bool CanHaveBody { get; }
}
}

View File

@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
internal class FeatureContext : internal class FeatureContext :
IHttpRequestFeature, IHttpRequestFeature,
IHttpRequestBodyDetectionFeature,
IHttpConnectionFeature, IHttpConnectionFeature,
IHttpResponseFeature, IHttpResponseFeature,
IHttpResponseBodyFeature, IHttpResponseBodyFeature,
@ -212,6 +213,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
set { _scheme = value; } set { _scheme = value; }
} }
bool IHttpRequestBodyDetectionFeature.CanHaveBody => Request.HasEntityBody;
IPAddress IHttpConnectionFeature.LocalIpAddress IPAddress IHttpConnectionFeature.LocalIpAddress
{ {
get get

View File

@ -135,6 +135,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ {
if (_contentBoundaryType == BoundaryType.None) 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]; string transferEncoding = Headers[HttpKnownHeaderNames.TransferEncoding];
if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase)) if (string.Equals("chunked", transferEncoding?.Trim(), StringComparison.OrdinalIgnoreCase))
{ {

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private static readonly Dictionary<Type, Func<FeatureContext, object>> _featureFuncLookup = new Dictionary<Type, Func<FeatureContext, object>>() private static readonly Dictionary<Type, Func<FeatureContext, object>> _featureFuncLookup = new Dictionary<Type, Func<FeatureContext, object>>()
{ {
{ typeof(IHttpRequestFeature), _identityFunc }, { typeof(IHttpRequestFeature), _identityFunc },
{ typeof(IHttpRequestBodyDetectionFeature), _identityFunc },
{ typeof(IHttpConnectionFeature), _identityFunc }, { typeof(IHttpConnectionFeature), _identityFunc },
{ typeof(IHttpResponseFeature), _identityFunc }, { typeof(IHttpResponseFeature), _identityFunc },
{ typeof(IHttpResponseBodyFeature), _identityFunc }, { typeof(IHttpResponseBodyFeature), _identityFunc },

View File

@ -2,9 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
@ -31,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext => using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
{ {
// Default 200 // Default 200
Assert.False(httpContext.Request.CanHaveBody());
return Task.CompletedTask; return Task.CompletedTask;
}); });
@ -64,6 +68,233 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
.Build().RunAsync(); .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<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "https"),
new KeyValuePair<string, string>(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] [ConditionalFact]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
public async Task ResponseWithData_Success() public async Task ResponseWithData_Success()

View File

@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>(); var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
Assert.NotNull(feature); Assert.NotNull(feature);
Assert.False(feature.IsReadOnly); Assert.False(feature.IsReadOnly);
Assert.True(httpContext.Request.CanHaveBody());
Assert.Equal(11, httpContext.Request.ContentLength); Assert.Equal(11, httpContext.Request.ContentLength);
byte[] input = new byte[100]; byte[] input = new byte[100];
int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
@ -114,6 +115,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>(); var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
Assert.NotNull(feature); Assert.NotNull(feature);
Assert.False(feature.IsReadOnly); Assert.False(feature.IsReadOnly);
Assert.True(httpContext.Request.CanHaveBody());
Assert.Null(httpContext.Request.ContentLength); Assert.Null(httpContext.Request.ContentLength);
byte[] input = new byte[100]; byte[] input = new byte[100];
int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);

View File

@ -9,6 +9,7 @@ using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address; string address;
using (Utilities.CreateHttpServer(out address, httpContext => using (Utilities.CreateHttpServer(out address, httpContext =>
{ {
Assert.True(httpContext.Request.CanHaveBody());
byte[] input = new byte[100]; byte[] input = new byte[100];
httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true; httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
int read = httpContext.Request.Body.Read(input, 0, input.Length); int read = httpContext.Request.Body.Read(input, 0, input.Length);
@ -42,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address; string address;
using (Utilities.CreateHttpServer(out address, async httpContext => using (Utilities.CreateHttpServer(out address, async httpContext =>
{ {
Assert.True(httpContext.Request.CanHaveBody());
byte[] input = new byte[100]; byte[] input = new byte[100];
int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length); int read = await httpContext.Request.Body.ReadAsync(input, 0, input.Length);
httpContext.Response.ContentLength = read; httpContext.Response.ContentLength = read;

View File

@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget); Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget);
Assert.Equal("HTTP/1.1", requestInfo.Protocol); Assert.Equal("HTTP/1.1", requestInfo.Protocol);
Assert.False(httpContext.Request.CanHaveBody());
var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>(); var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString()); Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString());
Assert.NotEqual(0, connectionInfo.RemotePort); Assert.NotEqual(0, connectionInfo.RemotePort);

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@ -170,5 +171,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout); internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);
internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout); internal static Task<T> WithTimeout<T>(this Task<T> task) => task.TimeoutAfter(DefaultTimeout);
internal static bool? CanHaveBody(this HttpRequest request)
{
return request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
}
} }
} }

View File

@ -18,6 +18,7 @@ using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
internal partial class HttpProtocol : IHttpRequestFeature, internal partial class HttpProtocol : IHttpRequestFeature,
IHttpRequestBodyDetectionFeature,
IHttpResponseFeature, IHttpResponseFeature,
IHttpResponseBodyFeature, IHttpResponseBodyFeature,
IRequestBodyPipeFeature, IRequestBodyPipeFeature,
@ -121,6 +122,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
} }
} }
bool IHttpRequestBodyDetectionFeature.CanHaveBody => _bodyControl.CanHaveBody;
bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable; bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable;
IHeaderDictionary IHttpRequestTrailersFeature.Trailers IHeaderDictionary IHttpRequestTrailersFeature.Trailers

View File

@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
internal partial class HttpProtocol : IFeatureCollection internal partial class HttpProtocol : IFeatureCollection
{ {
private object _currentIHttpRequestFeature; private object _currentIHttpRequestFeature;
private object _currentIHttpRequestBodyDetectionFeature;
private object _currentIHttpResponseFeature; private object _currentIHttpResponseFeature;
private object _currentIHttpResponseBodyFeature; private object _currentIHttpResponseBodyFeature;
private object _currentIRequestBodyPipeFeature; private object _currentIRequestBodyPipeFeature;
@ -48,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private void FastReset() private void FastReset()
{ {
_currentIHttpRequestFeature = this; _currentIHttpRequestFeature = this;
_currentIHttpRequestBodyDetectionFeature = this;
_currentIHttpResponseFeature = this; _currentIHttpResponseFeature = this;
_currentIHttpResponseBodyFeature = this; _currentIHttpResponseBodyFeature = this;
_currentIRequestBodyPipeFeature = this; _currentIRequestBodyPipeFeature = this;
@ -133,6 +135,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
feature = _currentIHttpRequestFeature; feature = _currentIHttpRequestFeature;
} }
else if (key == typeof(IHttpRequestBodyDetectionFeature))
{
feature = _currentIHttpRequestBodyDetectionFeature;
}
else if (key == typeof(IHttpResponseFeature)) else if (key == typeof(IHttpResponseFeature))
{ {
feature = _currentIHttpResponseFeature; feature = _currentIHttpResponseFeature;
@ -253,6 +259,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_currentIHttpRequestFeature = value; _currentIHttpRequestFeature = value;
} }
else if (key == typeof(IHttpRequestBodyDetectionFeature))
{
_currentIHttpRequestBodyDetectionFeature = value;
}
else if (key == typeof(IHttpResponseFeature)) else if (key == typeof(IHttpResponseFeature))
{ {
_currentIHttpResponseFeature = value; _currentIHttpResponseFeature = value;
@ -371,6 +381,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
feature = (TFeature)_currentIHttpRequestFeature; feature = (TFeature)_currentIHttpRequestFeature;
} }
else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature))
{
feature = (TFeature)_currentIHttpRequestBodyDetectionFeature;
}
else if (typeof(TFeature) == typeof(IHttpResponseFeature)) else if (typeof(TFeature) == typeof(IHttpResponseFeature))
{ {
feature = (TFeature)_currentIHttpResponseFeature; feature = (TFeature)_currentIHttpResponseFeature;
@ -495,6 +509,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
_currentIHttpRequestFeature = feature; _currentIHttpRequestFeature = feature;
} }
else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature))
{
_currentIHttpRequestBodyDetectionFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResponseFeature)) else if (typeof(TFeature) == typeof(IHttpResponseFeature))
{ {
_currentIHttpResponseFeature = feature; _currentIHttpResponseFeature = feature;
@ -611,6 +629,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{ {
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), _currentIHttpRequestFeature); yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), _currentIHttpRequestFeature);
} }
if (_currentIHttpRequestBodyDetectionFeature != null)
{
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestBodyDetectionFeature), _currentIHttpRequestBodyDetectionFeature);
}
if (_currentIHttpResponseFeature != null) if (_currentIHttpResponseFeature != null)
{ {
yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), _currentIHttpResponseFeature); yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), _currentIHttpResponseFeature);

View File

@ -36,6 +36,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
_upgradeStream = new HttpUpgradeStream(_request, _response); _upgradeStream = new HttpUpgradeStream(_request, _response);
} }
public bool CanHaveBody { get; private set; }
public Stream Upgrade() public Stream Upgrade()
{ {
// causes writes to context.Response.Body to throw // causes writes to context.Response.Body to throw
@ -46,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body) public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body)
{ {
CanHaveBody = !body.IsEmpty;
_requestReader.StartAcceptingReads(body); _requestReader.StartAcceptingReads(body);
_emptyRequestReader.StartAcceptingReads(MessageBody.ZeroContentLengthClose); _emptyRequestReader.StartAcceptingReads(MessageBody.ZeroContentLengthClose);
_responseWriter.StartAcceptingWrites(); _responseWriter.StartAcceptingWrites();

View File

@ -116,6 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void FeaturesSetByTypeSameAsGeneric() public void FeaturesSetByTypeSameAsGeneric()
{ {
_collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestBodyDetectionFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseBodyFeature)] = CreateHttp1Connection(); _collection[typeof(IHttpResponseBodyFeature)] = CreateHttp1Connection();
_collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection(); _collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection();
@ -139,6 +140,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
public void FeaturesSetByGenericSameAsByType() public void FeaturesSetByGenericSameAsByType()
{ {
_collection.Set<IHttpRequestFeature>(CreateHttp1Connection()); _collection.Set<IHttpRequestFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestBodyDetectionFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseFeature>(CreateHttp1Connection()); _collection.Set<IHttpResponseFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseBodyFeature>(CreateHttp1Connection()); _collection.Set<IHttpResponseBodyFeature>(CreateHttp1Connection());
_collection.Set<IRequestBodyPipeFeature>(CreateHttp1Connection()); _collection.Set<IRequestBodyPipeFeature>(CreateHttp1Connection());
@ -194,6 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
private void CompareGenericGetterToIndexer() private void CompareGenericGetterToIndexer()
{ {
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]); Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);
Assert.Same(_collection.Get<IHttpRequestBodyDetectionFeature>(), _collection[typeof(IHttpRequestBodyDetectionFeature)]);
Assert.Same(_collection.Get<IHttpResponseFeature>(), _collection[typeof(IHttpResponseFeature)]); Assert.Same(_collection.Get<IHttpResponseFeature>(), _collection[typeof(IHttpResponseFeature)]);
Assert.Same(_collection.Get<IHttpResponseBodyFeature>(), _collection[typeof(IHttpResponseBodyFeature)]); Assert.Same(_collection.Get<IHttpResponseBodyFeature>(), _collection[typeof(IHttpResponseBodyFeature)]);
Assert.Same(_collection.Get<IRequestBodyPipeFeature>(), _collection[typeof(IRequestBodyPipeFeature)]); Assert.Same(_collection.Get<IRequestBodyPipeFeature>(), _collection[typeof(IRequestBodyPipeFeature)]);

View File

@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var request = httpContext.Request; var request = httpContext.Request;
var response = httpContext.Response; var response = httpContext.Response;
Assert.True(request.CanHaveBody());
while (true) while (true)
{ {
var buffer = new byte[8192]; var buffer = new byte[8192];
@ -40,6 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var request = httpContext.Request; var request = httpContext.Request;
var response = httpContext.Response; var response = httpContext.Response;
Assert.True(request.CanHaveBody());
while (true) while (true)
{ {
var readResult = await request.BodyReader.ReadAsync(); var readResult = await request.BodyReader.ReadAsync();
@ -58,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var request = httpContext.Request; var request = httpContext.Request;
var response = httpContext.Response; var response = httpContext.Response;
Assert.True(request.CanHaveBody());
var data = new MemoryStream(); var data = new MemoryStream();
await request.Body.CopyToAsync(data); await request.Body.CopyToAsync(data);
var bytes = data.ToArray(); var bytes = data.ToArray();
@ -174,6 +177,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var response = httpContext.Response; var response = httpContext.Response;
var request = httpContext.Request; var request = httpContext.Request;
Assert.True(request.CanHaveBody());
Assert.Equal("POST", request.Method); Assert.Equal("POST", request.Method);
@ -229,6 +233,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var response = httpContext.Response; var response = httpContext.Response;
var request = httpContext.Request; var request = httpContext.Request;
Assert.True(request.CanHaveBody());
var buffer = new byte[200]; var buffer = new byte[200];
@ -356,6 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var response = httpContext.Response; var response = httpContext.Response;
var request = httpContext.Request; var request = httpContext.Request;
Assert.True(request.CanHaveBody());
// The first request is chunked with no trailers. // The first request is chunked with no trailers.
if (requestsReceived == 0) if (requestsReceived == 0)
@ -652,6 +658,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var response = httpContext.Response; var response = httpContext.Response;
var request = httpContext.Request; var request = httpContext.Request;
Assert.True(request.CanHaveBody());
var buffer = new byte[200]; var buffer = new byte[200];
@ -695,6 +702,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
{ {
var response = httpContext.Response; var response = httpContext.Response;
var request = httpContext.Request; var request = httpContext.Request;
Assert.True(request.CanHaveBody());
var buffer = new byte[200]; var buffer = new byte[200];

View File

@ -64,22 +64,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false); await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
} }
[Fact] [Theory]
public async Task HEADERS_Received_CustomMethod_Accepted() [InlineData("GET")]
[InlineData("POST")]
[InlineData("PUT")]
[InlineData("PATCH")]
[InlineData("DELETE")]
[InlineData("CUSTOM")]
public async Task HEADERS_Received_KnownOrCustomMethods_Accepted(string method)
{ {
var headers = new[] var headers = new[]
{ {
new KeyValuePair<string, string>(HeaderNames.Method, "Custom"), new KeyValuePair<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"), new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"), new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"), new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
}; };
await InitializeConnectionAsync(_echoMethod); await InitializeConnectionAsync(_echoMethodNoBody);
await StartStreamAsync(1, headers, endStream: true); await StartStreamAsync(1, headers, endStream: true);
var headersFrame = await ExpectAsync(Http2FrameType.HEADERS, var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
withLength: 51, withLength: 45 + method.Length,
withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM), withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
withStreamId: 1); withStreamId: 1);
@ -90,14 +96,146 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Equal(4, _decodedHeaders.Count); Assert.Equal(4, _decodedHeaders.Count);
Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase); Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
Assert.Equal("200", _decodedHeaders[HeaderNames.Status]); Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
Assert.Equal("Custom", _decodedHeaders["Method"]); Assert.Equal("0", _decodedHeaders[HeaderNames.ContentLength]);
Assert.Equal("0", _decodedHeaders["content-length"]); Assert.Equal(method, _decodedHeaders["Method"]);
}
[Fact]
public async Task HEADERS_Received_HEADMethod_Accepted()
{
await InitializeConnectionAsync(_echoMethodNoBody);
var headers = new[]
{
new KeyValuePair<string, string>(HeaderNames.Method, "HEAD"),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
new KeyValuePair<string, string>(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<string, string>(HeaderNames.Method, method),
new KeyValuePair<string, string>(HeaderNames.Path, "/"),
new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
new KeyValuePair<string, string>(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] [Fact]
public async Task HEADERS_Received_CONNECTMethod_Accepted() public async Task HEADERS_Received_CONNECTMethod_Accepted()
{ {
await InitializeConnectionAsync(_echoMethod); await InitializeConnectionAsync(_echoMethodNoBody);
// :path and :scheme are not allowed, :authority is optional // :path and :scheme are not allowed, :authority is optional
var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT") }; var headers = new[] { new KeyValuePair<string, string>(HeaderNames.Method, "CONNECT") };

View File

@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
protected readonly RequestDelegate _waitForAbortApplication; protected readonly RequestDelegate _waitForAbortApplication;
protected readonly RequestDelegate _waitForAbortFlushingApplication; protected readonly RequestDelegate _waitForAbortFlushingApplication;
protected readonly RequestDelegate _readRateApplication; protected readonly RequestDelegate _readRateApplication;
protected readonly RequestDelegate _echoMethod; protected readonly RequestDelegate _echoMethodNoBody;
protected readonly RequestDelegate _echoHost; protected readonly RequestDelegate _echoHost;
protected readonly RequestDelegate _echoPath; protected readonly RequestDelegate _echoPath;
protected readonly RequestDelegate _appAbort; protected readonly RequestDelegate _appAbort;
@ -346,8 +346,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
await stalledReadTask; await stalledReadTask;
}; };
_echoMethod = context => _echoMethodNoBody = context =>
{ {
Assert.False(context.Request.CanHaveBody());
context.Response.Headers["Method"] = context.Request.Method; context.Response.Headers["Method"] = context.Request.Method;
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
await using (var server = new TestServer(async context => await using (var server = new TestServer(async context =>
{ {
Assert.True(context.Request.CanHaveBody());
var buffer = new byte[1]; var buffer = new byte[1];
#pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete
requestRejectedEx = await Assert.ThrowsAsync<BadHttpRequestException>( requestRejectedEx = await Assert.ThrowsAsync<BadHttpRequestException>(

View File

@ -1933,7 +1933,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
var request = httpContext.Request; var request = httpContext.Request;
Assert.Equal("POST", request.Method); Assert.Equal("POST", request.Method);
Assert.True(request.CanHaveBody());
var readResult = await request.BodyReader.ReadAsync(); var readResult = await request.BodyReader.ReadAsync();
request.BodyReader.AdvanceTo(readResult.Buffer.End); request.BodyReader.AdvanceTo(readResult.Buffer.End);

View File

@ -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<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
}
}
}

View File

@ -12,6 +12,7 @@ namespace CodeGenerator
var alwaysFeatures = new[] var alwaysFeatures = new[]
{ {
"IHttpRequestFeature", "IHttpRequestFeature",
"IHttpRequestBodyDetectionFeature",
"IHttpResponseFeature", "IHttpResponseFeature",
"IHttpResponseBodyFeature", "IHttpResponseBodyFeature",
"IRequestBodyPipeFeature", "IRequestBodyPipeFeature",
@ -58,6 +59,7 @@ namespace CodeGenerator
var implementedFeatures = new[] var implementedFeatures = new[]
{ {
"IHttpRequestFeature", "IHttpRequestFeature",
"IHttpRequestBodyDetectionFeature",
"IHttpResponseFeature", "IHttpResponseFeature",
"IHttpResponseBodyFeature", "IHttpResponseBodyFeature",
"IRequestBodyPipeFeature", "IRequestBodyPipeFeature",