parent
df37d00307
commit
11bae8a112
|
|
@ -66,32 +66,35 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
|
||||
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 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
|
||||
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)
|
||||
{
|
||||
// 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.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();
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +111,9 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
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 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
}
|
||||
|
||||
[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 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
}
|
||||
|
||||
[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 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.AspNetCore.TestHost
|
|||
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 bool? CanHaveBody(this HttpRequest request)
|
||||
{
|
||||
return request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
internal class FeatureContext :
|
||||
IHttpRequestFeature,
|
||||
IHttpRequestBodyDetectionFeature,
|
||||
IHttpConnectionFeature,
|
||||
IHttpResponseFeature,
|
||||
IHttpResponseBodyFeature,
|
||||
|
|
@ -212,6 +213,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
set { _scheme = value; }
|
||||
}
|
||||
|
||||
bool IHttpRequestBodyDetectionFeature.CanHaveBody => Request.HasEntityBody;
|
||||
|
||||
IPAddress IHttpConnectionFeature.LocalIpAddress
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
{
|
||||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
private static readonly Dictionary<Type, Func<FeatureContext, object>> _featureFuncLookup = new Dictionary<Type, Func<FeatureContext, object>>()
|
||||
{
|
||||
{ typeof(IHttpRequestFeature), _identityFunc },
|
||||
{ typeof(IHttpRequestBodyDetectionFeature), _identityFunc },
|
||||
{ typeof(IHttpConnectionFeature), _identityFunc },
|
||||
{ typeof(IHttpResponseFeature), _identityFunc },
|
||||
{ typeof(IHttpResponseBodyFeature), _identityFunc },
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
|
|||
using var server = Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
|
||||
{
|
||||
// Default 200
|
||||
Assert.False(httpContext.Request.CanHaveBody());
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
|
|
@ -64,6 +68,233 @@ namespace Microsoft.AspNetCore.Server.HttpSys.FunctionalTests
|
|||
.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]
|
||||
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10, SkipReason = "Http2 requires Win10")]
|
||||
public async Task ResponseWithData_Success()
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
var feature = httpContext.Features.Get<IHttpMaxRequestBodySizeFeature>();
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Net.Sockets;
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
string address;
|
||||
using (Utilities.CreateHttpServer(out address, httpContext =>
|
||||
{
|
||||
Assert.True(httpContext.Request.CanHaveBody());
|
||||
byte[] input = new byte[100];
|
||||
httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
|
||||
int read = httpContext.Request.Body.Read(input, 0, input.Length);
|
||||
|
|
@ -42,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
Assert.Equal("/basepath/SomePath?SomeQuery", requestInfo.RawTarget);
|
||||
Assert.Equal("HTTP/1.1", requestInfo.Protocol);
|
||||
|
||||
Assert.False(httpContext.Request.CanHaveBody());
|
||||
var connectionInfo = httpContext.Features.Get<IHttpConnectionFeature>();
|
||||
Assert.Equal("::1", connectionInfo.RemoteIpAddress.ToString());
|
||||
Assert.NotEqual(0, connectionInfo.RemotePort);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Hosting;
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.HttpSys
|
|||
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 bool? CanHaveBody(this HttpRequest request)
|
||||
{
|
||||
return request.HttpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ using Microsoft.Net.Http.Headers;
|
|||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
internal partial class HttpProtocol : IHttpRequestFeature,
|
||||
IHttpRequestBodyDetectionFeature,
|
||||
IHttpResponseFeature,
|
||||
IHttpResponseBodyFeature,
|
||||
IRequestBodyPipeFeature,
|
||||
|
|
@ -121,6 +122,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
}
|
||||
}
|
||||
|
||||
bool IHttpRequestBodyDetectionFeature.CanHaveBody => _bodyControl.CanHaveBody;
|
||||
|
||||
bool IHttpRequestTrailersFeature.Available => RequestTrailersAvailable;
|
||||
|
||||
IHeaderDictionary IHttpRequestTrailersFeature.Trailers
|
||||
|
|
|
|||
|
|
@ -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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
private void FastReset()
|
||||
{
|
||||
_currentIHttpRequestFeature = this;
|
||||
_currentIHttpRequestBodyDetectionFeature = this;
|
||||
_currentIHttpResponseFeature = this;
|
||||
_currentIHttpResponseBodyFeature = this;
|
||||
_currentIRequestBodyPipeFeature = this;
|
||||
|
|
@ -133,6 +135,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
feature = _currentIHttpRequestFeature;
|
||||
}
|
||||
else if (key == typeof(IHttpRequestBodyDetectionFeature))
|
||||
{
|
||||
feature = _currentIHttpRequestBodyDetectionFeature;
|
||||
}
|
||||
else if (key == typeof(IHttpResponseFeature))
|
||||
{
|
||||
feature = _currentIHttpResponseFeature;
|
||||
|
|
@ -253,6 +259,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
_currentIHttpRequestFeature = value;
|
||||
}
|
||||
else if (key == typeof(IHttpRequestBodyDetectionFeature))
|
||||
{
|
||||
_currentIHttpRequestBodyDetectionFeature = value;
|
||||
}
|
||||
else if (key == typeof(IHttpResponseFeature))
|
||||
{
|
||||
_currentIHttpResponseFeature = value;
|
||||
|
|
@ -371,6 +381,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
_currentIHttpRequestFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpRequestBodyDetectionFeature))
|
||||
{
|
||||
_currentIHttpRequestBodyDetectionFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpResponseFeature))
|
||||
{
|
||||
_currentIHttpResponseFeature = feature;
|
||||
|
|
@ -611,6 +629,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
{
|
||||
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), _currentIHttpRequestFeature);
|
||||
}
|
||||
if (_currentIHttpRequestBodyDetectionFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestBodyDetectionFeature), _currentIHttpRequestBodyDetectionFeature);
|
||||
}
|
||||
if (_currentIHttpResponseFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), _currentIHttpResponseFeature);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
_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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
|||
|
||||
public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body)
|
||||
{
|
||||
CanHaveBody = !body.IsEmpty;
|
||||
_requestReader.StartAcceptingReads(body);
|
||||
_emptyRequestReader.StartAcceptingReads(MessageBody.ZeroContentLengthClose);
|
||||
_responseWriter.StartAcceptingWrites();
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
public void FeaturesSetByGenericSameAsByType()
|
||||
{
|
||||
_collection.Set<IHttpRequestFeature>(CreateHttp1Connection());
|
||||
_collection.Set<IHttpRequestBodyDetectionFeature>(CreateHttp1Connection());
|
||||
_collection.Set<IHttpResponseFeature>(CreateHttp1Connection());
|
||||
_collection.Set<IHttpResponseBodyFeature>(CreateHttp1Connection());
|
||||
_collection.Set<IRequestBodyPipeFeature>(CreateHttp1Connection());
|
||||
|
|
@ -194,6 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
private void CompareGenericGetterToIndexer()
|
||||
{
|
||||
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<IHttpResponseBodyFeature>(), _collection[typeof(IHttpResponseBodyFeature)]);
|
||||
Assert.Same(_collection.Get<IRequestBodyPipeFeature>(), _collection[typeof(IRequestBodyPipeFeature)]);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
var request = httpContext.Request;
|
||||
var response = httpContext.Response;
|
||||
Assert.True(request.CanHaveBody());
|
||||
while (true)
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
|
|
@ -40,6 +41,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
var request = httpContext.Request;
|
||||
var response = httpContext.Response;
|
||||
Assert.True(request.CanHaveBody());
|
||||
while (true)
|
||||
{
|
||||
var readResult = await request.BodyReader.ReadAsync();
|
||||
|
|
@ -58,6 +60,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
Assert.True(request.CanHaveBody());
|
||||
|
||||
Assert.Equal("POST", request.Method);
|
||||
|
||||
|
|
@ -229,6 +233,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
Assert.True(request.CanHaveBody());
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
|
|
@ -356,6 +361,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
Assert.True(request.CanHaveBody());
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
|
|
@ -695,6 +702,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
{
|
||||
var response = httpContext.Response;
|
||||
var request = httpContext.Request;
|
||||
Assert.True(request.CanHaveBody());
|
||||
|
||||
var buffer = new byte[200];
|
||||
|
||||
|
|
|
|||
|
|
@ -64,22 +64,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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<string, string>(HeaderNames.Method, "Custom"),
|
||||
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(_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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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<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]
|
||||
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<string, string>(HeaderNames.Method, "CONNECT") };
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
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 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
|||
await stalledReadTask;
|
||||
};
|
||||
|
||||
_echoMethod = context =>
|
||||
_echoMethodNoBody = context =>
|
||||
{
|
||||
Assert.False(context.Request.CanHaveBody());
|
||||
context.Response.Headers["Method"] = context.Request.Method;
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
|
||||
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<BadHttpRequestException>(
|
||||
|
|
|
|||
|
|
@ -1933,7 +1933,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
|
|||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ namespace CodeGenerator
|
|||
var alwaysFeatures = new[]
|
||||
{
|
||||
"IHttpRequestFeature",
|
||||
"IHttpRequestBodyDetectionFeature",
|
||||
"IHttpResponseFeature",
|
||||
"IHttpResponseBodyFeature",
|
||||
"IRequestBodyPipeFeature",
|
||||
|
|
@ -58,6 +59,7 @@ namespace CodeGenerator
|
|||
var implementedFeatures = new[]
|
||||
{
|
||||
"IHttpRequestFeature",
|
||||
"IHttpRequestBodyDetectionFeature",
|
||||
"IHttpResponseFeature",
|
||||
"IHttpResponseBodyFeature",
|
||||
"IRequestBodyPipeFeature",
|
||||
|
|
|
|||
Loading…
Reference in New Issue