Change consumes behavior to ignore requests with no content type (#4459)
This commit is contained in:
parent
dc718f6602
commit
c6fa808a91
|
|
@ -76,7 +76,10 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
|
||||
// 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 (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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Formatters;
|
|||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Microsoft.AspNetCore.Routing.Patterns;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
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
|
||||
// endpoint that always returns a 415.
|
||||
if (!edges.ContainsKey(AnyContentType))
|
||||
if (!edges.TryGetValue(AnyContentType, out var anyEndpoints))
|
||||
{
|
||||
edges.Add(AnyContentType, new List<Endpoint>()
|
||||
{
|
||||
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
|
||||
.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
|
||||
// are then then execute them in linear order.
|
||||
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))
|
||||
.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)
|
||||
|
|
@ -207,21 +239,24 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
|
||||
private class ConsumesPolicyJumpTable : PolicyJumpTable
|
||||
{
|
||||
private (MediaType mediaType, int destination)[] _destinations;
|
||||
private int _exitDestination;
|
||||
private readonly (MediaType mediaType, int destination)[] _destinations;
|
||||
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;
|
||||
_noContentTypeDestination = noContentTypeDestination;
|
||||
_destinations = destinations;
|
||||
}
|
||||
|
||||
public override int GetDestination(HttpContext httpContext)
|
||||
{
|
||||
var contentType = httpContext.Request.ContentType;
|
||||
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
return _exitDestination;
|
||||
return _noContentTypeDestination;
|
||||
}
|
||||
|
||||
var requestMediaType = new MediaType(contentType);
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void OnResourceExecuting_NullOrEmptyRequestContentType_SetsUnsupportedMediaTypeResult(string contentType)
|
||||
public void OnResourceExecuting_NullOrEmptyRequestContentType_IsNoOp(string contentType)
|
||||
{
|
||||
// Arrange
|
||||
var httpContext = new DefaultHttpContext();
|
||||
|
|
@ -348,8 +348,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
consumesFilter.OnResourceExecuting(resourceExecutingContext);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resourceExecutingContext.Result);
|
||||
Assert.IsType<UnsupportedMediaTypeResult>(resourceExecutingContext.Result);
|
||||
Assert.Null(resourceExecutingContext.Result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -91,6 +91,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
Assert.Collection(
|
||||
edges.OrderBy(e => e.State),
|
||||
e =>
|
||||
{
|
||||
Assert.Equal(string.Empty, e.State);
|
||||
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
|
||||
},
|
||||
e =>
|
||||
{
|
||||
Assert.Equal("*/*", e.State);
|
||||
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
|
||||
public void GetEdges_GroupsByContentType_CreatesHttp405Endpoint()
|
||||
public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint()
|
||||
{
|
||||
// Arrange
|
||||
var endpoints = new[]
|
||||
|
|
@ -144,6 +149,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
Assert.Collection(
|
||||
edges.OrderBy(e => e.State),
|
||||
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(ConsumesMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName);
|
||||
|
|
@ -190,6 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
|
|||
var edges = new PolicyJumpTableEdge[]
|
||||
{
|
||||
// In reverse order of how they should be processed
|
||||
new PolicyJumpTableEdge(string.Empty, 0),
|
||||
new PolicyJumpTableEdge("*/*", 1),
|
||||
new PolicyJumpTableEdge("application/*", 2),
|
||||
new PolicyJumpTableEdge("text/*", 3),
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
// Arrange
|
||||
var request = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"http://localhost/ConsumesAttribute_Company/CreateProduct");
|
||||
"http://localhost/ConsumesAttribute_WithFallbackActionController/CreateProduct");
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
|
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent_ReturnsUnsupported()
|
||||
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
|
||||
{
|
||||
// Arrange
|
||||
var request = new HttpRequestMessage(
|
||||
|
|
@ -58,7 +58,26 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
|
||||
// Act
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -14,5 +14,17 @@ namespace BasicWebSite.Controllers.ActionConstraints
|
|||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
|
||||
namespace BasicWebSite.Controllers.ActionConstraints
|
||||
{
|
||||
[Route("ConsumesAttribute_Company/[action]")]
|
||||
[Route("ConsumesAttribute_WithFallbackActionController/[action]")]
|
||||
public class ConsumesAttribute_WithFallbackActionController : Controller
|
||||
{
|
||||
[Consumes("application/json")]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ namespace BasicWebSite
|
|||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
// Initializes the RequestId service for each request
|
||||
app.UseMiddleware<RequestIdMiddleware>();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue