Change consumes behavior to ignore requests with no content type (#6645)
This commit is contained in:
parent
0622513058
commit
92680b355f
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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>();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue