Skip to content

Commit 92680b3

Browse files
authored
Change consumes behavior to ignore requests with no content type (#6645)
1 parent 0622513 commit 92680b3

File tree

9 files changed

+98
-16
lines changed

9 files changed

+98
-16
lines changed

eng/PatchConfig.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Later on, this will be checked using this condition:
3232
Microsoft.AspNetCore.Authentication.Google;
3333
Microsoft.AspNetCore.Http;
3434
Microsoft.AspNetCore.Mvc.Core;
35+
Microsoft.AspNetCore.Routing;
3536
Microsoft.AspNetCore.Server.IIS;
3637
Microsoft.AspNetCore.Server.Kestrel.Core;
3738
java:signalr;

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/ConsumesAttribute.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ public void OnResourceExecuting(ResourceExecutingContext context)
7777

7878
// Confirm the request's content type is more specific than a media type this action supports e.g. OK
7979
// if client sent "text/plain" data and this action supports "text/*".
80-
if (requestContentType == null || !IsSubsetOfAnyContentType(requestContentType))
80+
//
81+
// Requests without a content type do not return a 415. It is a common pattern to place [Consumes] on
82+
// a controller and have GET actions
83+
if (requestContentType != null && !IsSubsetOfAnyContentType(requestContentType))
8184
{
8285
context.Result = new UnsupportedMediaTypeResult();
8386
}

src/Mvc/src/Microsoft.AspNetCore.Mvc.Core/Routing/ConsumesMatcherPolicy.cs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.AspNetCore.Routing;
1111
using Microsoft.AspNetCore.Routing.Matching;
1212
using Microsoft.AspNetCore.Routing.Patterns;
13+
using Microsoft.Extensions.Primitives;
1314

1415
namespace Microsoft.AspNetCore.Mvc.Routing
1516
{
@@ -120,13 +121,23 @@ public IReadOnlyList<PolicyNodeEdge> GetEdges(IReadOnlyList<Endpoint> endpoints)
120121

121122
// If after we're done there isn't any endpoint that accepts */*, then we'll synthesize an
122123
// endpoint that always returns a 415.
123-
if (!edges.ContainsKey(AnyContentType))
124+
if (!edges.TryGetValue(AnyContentType, out var anyEndpoints))
124125
{
125126
edges.Add(AnyContentType, new List<Endpoint>()
126127
{
127128
CreateRejectionEndpoint(),
128129
});
130+
131+
// Add a node to use when there is no request content type.
132+
// When there is no content type we want the policy to no-op
133+
edges.Add(string.Empty, endpoints.ToList());
129134
}
135+
else
136+
{
137+
// If there is an endpoint that accepts */* then it is also used when there is no content type
138+
edges.Add(string.Empty, anyEndpoints.ToList());
139+
}
140+
130141

131142
return edges
132143
.Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value))
@@ -155,7 +166,7 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
155166
// Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they
156167
// are then then execute them in linear order.
157168
var ordered = edges
158-
.Select(e => (mediaType: new MediaType((string)e.State), destination: e.Destination))
169+
.Select(e => (mediaType: CreateEdgeMediaType(ref e), destination: e.Destination))
159170
.OrderBy(e => GetScore(e.mediaType))
160171
.ToArray();
161172

@@ -170,7 +181,28 @@ public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList<PolicyJ
170181
}
171182
}
172183

173-
return new ConsumesPolicyJumpTable(exitDestination, ordered);
184+
var noContentTypeDestination = GetNoContentTypeDestination(ordered);
185+
186+
return new ConsumesPolicyJumpTable(exitDestination, noContentTypeDestination, ordered);
187+
}
188+
189+
private static int GetNoContentTypeDestination((MediaType mediaType, int destination)[] destinations)
190+
{
191+
for (var i = 0; i < destinations.Length; i++)
192+
{
193+
if (!destinations[i].mediaType.Type.HasValue)
194+
{
195+
return destinations[i].destination;
196+
}
197+
}
198+
199+
throw new InvalidOperationException("Could not find destination for no content type.");
200+
}
201+
202+
private static MediaType CreateEdgeMediaType(ref PolicyJumpTableEdge e)
203+
{
204+
var mediaType = (string)e.State;
205+
return !string.IsNullOrEmpty(mediaType) ? new MediaType(mediaType) : default;
174206
}
175207

176208
private int GetScore(in MediaType mediaType)
@@ -207,21 +239,24 @@ protected override int CompareMetadata(IConsumesMetadata x, IConsumesMetadata y)
207239

208240
private class ConsumesPolicyJumpTable : PolicyJumpTable
209241
{
210-
private (MediaType mediaType, int destination)[] _destinations;
211-
private int _exitDestination;
242+
private readonly (MediaType mediaType, int destination)[] _destinations;
243+
private readonly int _exitDestination;
244+
private readonly int _noContentTypeDestination;
212245

213-
public ConsumesPolicyJumpTable(int exitDestination, (MediaType mediaType, int destination)[] destinations)
246+
public ConsumesPolicyJumpTable(int exitDestination, int noContentTypeDestination, (MediaType mediaType, int destination)[] destinations)
214247
{
215248
_exitDestination = exitDestination;
249+
_noContentTypeDestination = noContentTypeDestination;
216250
_destinations = destinations;
217251
}
218252

219253
public override int GetDestination(HttpContext httpContext)
220254
{
221255
var contentType = httpContext.Request.ContentType;
256+
222257
if (string.IsNullOrEmpty(contentType))
223258
{
224-
return _exitDestination;
259+
return _noContentTypeDestination;
225260
}
226261

227262
var requestMediaType = new MediaType(contentType);

src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/ConsumesAttributeTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ public void OnResourceExecuting_ForNoContentTypeMatch_SetsUnsupportedMediaTypeRe
326326
[Theory]
327327
[InlineData("")]
328328
[InlineData(null)]
329-
public void OnResourceExecuting_NullOrEmptyRequestContentType_SetsUnsupportedMediaTypeResult(string contentType)
329+
public void OnResourceExecuting_NullOrEmptyRequestContentType_IsNoOp(string contentType)
330330
{
331331
// Arrange
332332
var httpContext = new DefaultHttpContext();
@@ -349,8 +349,7 @@ public void OnResourceExecuting_NullOrEmptyRequestContentType_SetsUnsupportedMed
349349
consumesFilter.OnResourceExecuting(resourceExecutingContext);
350350

351351
// Assert
352-
Assert.NotNull(resourceExecutingContext.Result);
353-
Assert.IsType<UnsupportedMediaTypeResult>(resourceExecutingContext.Result);
352+
Assert.Null(resourceExecutingContext.Result);
354353
}
355354

356355
[Theory]

src/Mvc/test/Microsoft.AspNetCore.Mvc.Core.Test/Routing/ConsumesMatcherPolicyTest.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public void GetEdges_GroupsByContentType()
9191
Assert.Collection(
9292
edges.OrderBy(e => e.State),
9393
e =>
94+
{
95+
Assert.Equal(string.Empty, e.State);
96+
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
97+
},
98+
e =>
9499
{
95100
Assert.Equal("*/*", e.State);
96101
Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray());
@@ -123,7 +128,7 @@ public void GetEdges_GroupsByContentType()
123128
}
124129

125130
[Fact] // See explanation in GetEdges for how this case is different
126-
public void GetEdges_GroupsByContentType_CreatesHttp405Endpoint()
131+
public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint()
127132
{
128133
// Arrange
129134
var endpoints = new[]
@@ -144,6 +149,11 @@ public void GetEdges_GroupsByContentType_CreatesHttp405Endpoint()
144149
Assert.Collection(
145150
edges.OrderBy(e => e.State),
146151
e =>
152+
{
153+
Assert.Equal(string.Empty, e.State);
154+
Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray());
155+
},
156+
e =>
147157
{
148158
Assert.Equal("*/*", e.State);
149159
Assert.Equal(ConsumesMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName);
@@ -190,6 +200,7 @@ public void BuildJumpTable_SortsEdgesByPriority(string contentType, int expected
190200
var edges = new PolicyJumpTableEdge[]
191201
{
192202
// In reverse order of how they should be processed
203+
new PolicyJumpTableEdge(string.Empty, 0),
193204
new PolicyJumpTableEdge("*/*", 1),
194205
new PolicyJumpTableEdge("application/*", 2),
195206
new PolicyJumpTableEdge("text/*", 3),

src/Mvc/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public async Task NoRequestContentType_SelectsActionWithoutConstraint()
3737
// Arrange
3838
var request = new HttpRequestMessage(
3939
HttpMethod.Post,
40-
"http://localhost/ConsumesAttribute_Company/CreateProduct");
40+
"http://localhost/ConsumesAttribute_WithFallbackActionController/CreateProduct");
4141

4242
// Act
4343
var response = await Client.SendAsync(request);
@@ -49,7 +49,7 @@ public async Task NoRequestContentType_SelectsActionWithoutConstraint()
4949
}
5050

5151
[Fact]
52-
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent_ReturnsUnsupported()
52+
public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPresent()
5353
{
5454
// Arrange
5555
var request = new HttpRequestMessage(
@@ -58,7 +58,26 @@ public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPr
5858

5959
// Act
6060
var response = await Client.SendAsync(request);
61-
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
61+
var body = await response.Content.ReadAsStringAsync();
62+
63+
// Assert
64+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
65+
Assert.Equal("ConsumesAttribute_PassThrough_Product_Json", body);
66+
}
67+
68+
[Fact]
69+
public async Task NoRequestContentType_MultipleMatches_IfAMultipleActionWithConstraintIsPresent()
70+
{
71+
// Arrange
72+
var request = new HttpRequestMessage(
73+
HttpMethod.Post,
74+
"http://localhost/ConsumesAttribute_PassThrough/CreateProductMultiple");
75+
76+
// Act
77+
var response = await Client.SendAsync(request);
78+
79+
// Assert
80+
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
6281
}
6382

6483
[Theory]

src/Mvc/test/WebSites/BasicWebSite/Controllers/ActionConstraints/ConsumesAttribute_PassThroughController.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,17 @@ public IActionResult CreateProduct(Product_Json jsonInput)
1414
{
1515
return Content("ConsumesAttribute_PassThrough_Product_Json");
1616
}
17+
18+
[Consumes("application/json")]
19+
public IActionResult CreateProductMultiple(Product_Json jsonInput)
20+
{
21+
return Content("ConsumesAttribute_PassThrough_Product_Json");
22+
}
23+
24+
[Consumes("application/xml")]
25+
public IActionResult CreateProductMultiple(Product_Xml jsonInput)
26+
{
27+
return Content("ConsumesAttribute_PassThrough_Product_Xml");
28+
}
1729
}
1830
}

src/Mvc/test/WebSites/BasicWebSite/Controllers/ActionConstraints/ConsumesAttribute_WithFallbackActionController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace BasicWebSite.Controllers.ActionConstraints
88
{
9-
[Route("ConsumesAttribute_Company/[action]")]
9+
[Route("ConsumesAttribute_WithFallbackActionController/[action]")]
1010
public class ConsumesAttribute_WithFallbackActionController : Controller
1111
{
1212
[Consumes("application/json")]

src/Mvc/test/WebSites/BasicWebSite/StartupWithEndpointRouting.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public void ConfigureServices(IServiceCollection services)
2929

3030
public void Configure(IApplicationBuilder app)
3131
{
32+
app.UseDeveloperExceptionPage();
33+
3234
// Initializes the RequestId service for each request
3335
app.UseMiddleware<RequestIdMiddleware>();
3436

0 commit comments

Comments
 (0)