Change consumes behavior to ignore requests with no content type (#6645)

This commit is contained in:
James Newton-King 2019-01-16 11:55:54 +13:00 committed by GitHub
parent 0622513058
commit 92680b355f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 98 additions and 16 deletions

View File

@ -32,6 +32,7 @@ Later on, this will be checked using this condition:
Microsoft.AspNetCore.Authentication.Google; Microsoft.AspNetCore.Authentication.Google;
Microsoft.AspNetCore.Http; Microsoft.AspNetCore.Http;
Microsoft.AspNetCore.Mvc.Core; Microsoft.AspNetCore.Mvc.Core;
Microsoft.AspNetCore.Routing;
Microsoft.AspNetCore.Server.IIS; Microsoft.AspNetCore.Server.IIS;
Microsoft.AspNetCore.Server.Kestrel.Core; Microsoft.AspNetCore.Server.Kestrel.Core;
java:signalr; java:signalr;

View File

@ -77,7 +77,10 @@ namespace Microsoft.AspNetCore.Mvc
// Confirm the request's content type is more specific than a media type this action supports e.g. OK // Confirm the request's content type is more specific than a media type this action supports e.g. OK
// if client sent "text/plain" data and this action supports "text/*". // if client sent "text/plain" data and this action supports "text/*".
if (requestContentType == null || !IsSubsetOfAnyContentType(requestContentType)) //
// Requests without a content type do not return a 415. It is a common pattern to place [Consumes] on
// a controller and have GET actions
if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType))
{ {
context.Result = new UnsupportedMediaTypeResult(); context.Result = new UnsupportedMediaTypeResult();
} }

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.Routing namespace Microsoft.AspNetCore.Mvc.Routing
{ {
@ -120,13 +121,23 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an // If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an
// endpoint that always returns a 415. // endpoint that always returns a 415.
if (!edges.ContainsKey(AnyContentType)) if (!edges.TryGetValue(AnyContentType, out var anyEndpoints))
{ {
edges.Add(AnyContentType, new List<Endpoint>() edges.Add(AnyContentType, new List<Endpoint>()
{ {
CreateRejectionEndpoint(), CreateRejectionEndpoint(),
}); });
// Add a node to use when there is no request content type.
// When there is no content type we want the policy to no-op
edges.Add(string.Empty, endpoints.ToList());
} }
else
{
// If there is an endpoint that accepts */* then it is also used when there is no content type
edges.Add(string.Empty, anyEndpoints.ToList());
}
return edges return edges
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value)) .Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
@ -155,7 +166,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they // Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
// are then then execute them in linear order. // are then then execute them in linear order.
var ordered = edges var ordered = edges
.Select(e => (mediaType: new MediaType((string)e.State), destination: e.Destination)) .Select(e => (mediaType: CreateEdgeMediaType(ref e), destination: e.Destination))
.OrderBy(e => GetScore(e.mediaType)) .OrderBy(e => GetScore(e.mediaType))
.ToArray(); .ToArray();
@ -170,7 +181,28 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
} }
return new ConsumesPolicyJumpTable(exitDestination, ordered); var noContentTypeDestination = GetNoContentTypeDestination(ordered);
return new ConsumesPolicyJumpTable(exitDestination, noContentTypeDestination, ordered);
}
private static int GetNoContentTypeDestination((MediaType mediaType, int destination)[] destinations)
{
for (var i = 0; i < destinations.Length; i++)
{
if (!destinations[i].mediaType.Type.HasValue)
{
return destinations[i].destination;
}
}
throw new InvalidOperationException("Could not find destination for no content type.");
}
private static MediaType CreateEdgeMediaType(ref PolicyJumpTableEdge e)
{
var mediaType = (string)e.State;
return !string.IsNullOrEmpty(mediaType) ? new MediaType(mediaType) : default;
} }
private int GetScore(in MediaType mediaType) private int GetScore(in MediaType mediaType)
@ -207,21 +239,24 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private class ConsumesPolicyJumpTable : PolicyJumpTable private class ConsumesPolicyJumpTable : PolicyJumpTable
{ {
private (MediaType mediaType, int destination)[] _destinations; private readonly (MediaType mediaType, int destination)[] _destinations;
private int _exitDestination; private readonly int _exitDestination;
private readonly int _noContentTypeDestination;
public ConsumesPolicyJumpTable(int exitDestination, (MediaType mediaType, int destination)[] destinations) public ConsumesPolicyJumpTable(int exitDestination, int noContentTypeDestination, (MediaType mediaType, int destination)[] destinations)
{ {
_exitDestination = exitDestination; _exitDestination = exitDestination;
_noContentTypeDestination = noContentTypeDestination;
_destinations = destinations; _destinations = destinations;
} }
public override int GetDestination(HttpContext httpContext) public override int GetDestination(HttpContext httpContext)
{ {
var contentType = httpContext.Request.ContentType; var contentType = httpContext.Request.ContentType;
if (string.IsNullOrEmpty(contentType)) if (string.IsNullOrEmpty(contentType))
{ {
return _exitDestination; return _noContentTypeDestination;
} }
var requestMediaType = new MediaType(contentType); var requestMediaType = new MediaType(contentType);

View File

@ -326,7 +326,7 @@ namespace Microsoft.AspNetCore.Mvc
[Theory] [Theory]
[InlineData("")] [InlineData("")]
[InlineData(null)] [InlineData(null)]
public void OnResourceExecuting_NullOrEmptyRequestContentType_SetsUnsupportedMediaTypeResult(string contentType) public void OnResourceExecuting_NullOrEmptyRequestContentType_IsNoOp(string contentType)
{ {
// Arrange // Arrange
var httpContext = new DefaultHttpContext(); var httpContext = new DefaultHttpContext();
@ -349,8 +349,7 @@ namespace Microsoft.AspNetCore.Mvc
consumesFilter.OnResourceExecuting(resourceExecutingContext); consumesFilter.OnResourceExecuting(resourceExecutingContext);
// Assert // Assert
Assert.NotNull(resourceExecutingContext.Result); Assert.Null(resourceExecutingContext.Result);
Assert.IsType<UnsupportedMediaTypeResult>(resourceExecutingContext.Result);
} }
[Theory] [Theory]

View File

@ -91,6 +91,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
Assert.Collection( Assert.Collection(
edges.OrderBy(e => e.State), edges.OrderBy(e => e.State),
e => e =>
{
Assert.Equal(string.Empty, e.State);
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
},
e =>
{ {
Assert.Equal("*/*", e.State); Assert.Equal("*/*", e.State);
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
@ -123,7 +128,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
} }
[Fact] // See explanation in GetEdges for how this case is different [Fact] // See explanation in GetEdges for how this case is different
public void GetEdges_GroupsByContentType_CreatesHttp405Endpoint() public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint()
{ {
// Arrange // Arrange
var endpoints = new[] var endpoints = new[]
@ -144,6 +149,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
Assert.Collection( Assert.Collection(
edges.OrderBy(e => e.State), edges.OrderBy(e => e.State),
e => e =>
{
Assert.Equal(string.Empty, e.State);
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
},
e =>
{ {
Assert.Equal("*/*", e.State); Assert.Equal("*/*", e.State);
Assert.Equal(ConsumesMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName); Assert.Equal(ConsumesMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName);
@ -190,6 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var edges = new PolicyJumpTableEdge[] var edges = new PolicyJumpTableEdge[]
{ {
// In reverse order of how they should be processed // In reverse order of how they should be processed
new PolicyJumpTableEdge(string.Empty, 0),
new PolicyJumpTableEdge("*/*", 1), new PolicyJumpTableEdge("*/*", 1),
new PolicyJumpTableEdge("application/*", 2), new PolicyJumpTableEdge("application/*", 2),
new PolicyJumpTableEdge("text/*", 3), new PolicyJumpTableEdge("text/*", 3),

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Arrange // Arrange
var request = new HttpRequestMessage( var request = new HttpRequestMessage(
HttpMethod.Post, HttpMethod.Post,
"http://localhost/ConsumesAttribute_Company/CreateProduct"); "http://localhost/ConsumesAttribute_WithFallbackActionController/CreateProduct");
// Act // Act
var response = await Client.SendAsync(request); var response = await Client.SendAsync(request);
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
} }
[Fact] [Fact]
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent_ReturnsUnsupported() public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
{ {
// Arrange // Arrange
var request = new HttpRequestMessage( var request = new HttpRequestMessage(
@ -58,7 +58,26 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Act // Act
var response = await Client.SendAsync(request); var response = await Client.SendAsync(request);
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); var body = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("ConsumesAttribute_PassThrough_Product_Json", body);
}
[Fact]
public async Task NoRequestContentType_MultipleMatches_IfAMultipleActionWithConstraintIsPresent()
{
// Arrange
var request = new HttpRequestMessage(
HttpMethod.Post,
"http://localhost/ConsumesAttribute_PassThrough/CreateProductMultiple");
// Act
var response = await Client.SendAsync(request);
// Assert
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
} }
[Theory] [Theory]

View File

@ -14,5 +14,17 @@ namespace BasicWebSite.Controllers.ActionConstraints
{ {
return Content("ConsumesAttribute_PassThrough_Product_Json"); return Content("ConsumesAttribute_PassThrough_Product_Json");
} }
[Consumes("application/json")]
public IActionResult CreateProductMultiple(Product_Json jsonInput)
{
return Content("ConsumesAttribute_PassThrough_Product_Json");
}
[Consumes("application/xml")]
public IActionResult CreateProductMultiple(Product_Xml jsonInput)
{
return Content("ConsumesAttribute_PassThrough_Product_Xml");
}
} }
} }

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers.ActionConstraints namespace BasicWebSite.Controllers.ActionConstraints
{ {
[Route("ConsumesAttribute_Company/[action]")] [Route("ConsumesAttribute_WithFallbackActionController/[action]")]
public class ConsumesAttribute_WithFallbackActionController : Controller public class ConsumesAttribute_WithFallbackActionController : Controller
{ {
[Consumes("application/json")] [Consumes("application/json")]

View File

@ -29,6 +29,8 @@ namespace BasicWebSite
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
app.UseDeveloperExceptionPage();
// Initializes the RequestId service for each request // Initializes the RequestId service for each request
app.UseMiddleware<RequestIdMiddleware>(); app.UseMiddleware<RequestIdMiddleware>();