Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
[Fixes #3874] Null passed as arguments to controller method parameter…
Browse files Browse the repository at this point in the history
…s when no InputFormatter matches

* Add an UnsupportedContentType to the ModelState dictionary when no formatter can read the body.
* Add a filter to the pipeline that searches for that specific exception and transforms the response into 415.
  • Loading branch information
javiercn committed Jan 28, 2016
1 parent 8753c60 commit 7cbb263
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public static void ConfigureMvc(MvcOptions options, IHttpRequestStreamReaderFact
options.ModelBinders.Add(new GenericModelBinder());
options.ModelBinders.Add(new MutableObjectModelBinder());

// Set up filters
options.Filters.Add(new UnsupportedContentTypeFilter());

// Set up default output formatters.
options.OutputFormatters.Add(new HttpNoContentOutputFormatter());
options.OutputFormatters.Add(new StringOutputFormatter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ private async Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bi

if (formatter == null)
{
var unsupportedContentType = Resources.FormatUnsupportedContentType(
var message = Resources.FormatUnsupportedContentType(
bindingContext.OperationBindingContext.HttpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(modelBindingKey, unsupportedContentType);

var exception = new UnsupportedContentTypeException(message);
bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);

// This model binder is the only handler for the Body binding source and it cannot run twice. Always
// tell the model binding system to skip other model binders and never to fall back i.e. indicate a
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

using System;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// The <see cref="Exception"/> that is added to model state when a model binder for the body of the request is
/// unable to understand the request content type header.
/// </summary>
public class UnsupportedContentTypeException : Exception
{
/// <summary>
/// Creates a new instance of <see cref="UnsupportedContentTypeException"/> with the specified
/// exception <paramref name="message"/>.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public UnsupportedContentTypeException(string message)
: base(message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
/// <summary>
/// A filter that scans for <see cref="UnsupportedContentTypeException"/> in the
/// <see cref="ActionContext.ModelState"/> and shortcircuits the pipeline
/// with an Unsupported Media Type (415) response.
/// </summary>
public class UnsupportedContentTypeFilter : IActionFilter
{
/// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context)
{
if (HasUnsupportedContentTypeError(context))
{
context.Result = new UnsupportedMediaTypeResult();
}
}

private bool HasUnsupportedContentTypeError(ActionExecutingContext context)
{
var modelState = context.ModelState;
if (modelState.IsValid)
{
return false;
}

foreach (var kvp in modelState)
{
var errors = kvp.Value.Errors;
for (int i = 0; i < errors.Count; i++)
{
var error = errors[i];
if (error.Exception is UnsupportedContentTypeException)
{
return true;
}
}
}

return false;
}

/// <inheritdoc />
public void OnActionExecuted(ActionExecutedContext context)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class UnsupportedContentTypeFilterTest
{
[Fact]
public void OnActionExecuting_ChangesActionResult_IfUnsupportedContentTypeExceptionIsFoundOnModelState()
{
// Arrange
var context = new ActionExecutingContext(
new ActionContext
{
HttpContext = new DefaultHttpContext(),
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor()
},
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());

var modelMetadata = new EmptyModelMetadataProvider()
.GetMetadataForType(typeof(int));

context.ModelState.AddModelError(
"person.body",
new UnsupportedContentTypeException("error"),
modelMetadata);

var filter = new UnsupportedContentTypeFilter();

// Act
filter.OnActionExecuting(context);

// Assert
Assert.NotNull(context.Result);
var status = Assert.IsType<UnsupportedMediaTypeResult>(context.Result);
}

[Fact]
public void OnActionExecuting_DoesNotChangeActionResult_IfOtherErrorsAreFoundOnModelState()
{
// Arrange
var context = new ActionExecutingContext(
new ActionContext
{
HttpContext = new DefaultHttpContext(),
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor()
},
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());

context.ModelState.AddModelError("person.body", "Some error");

var filter = new UnsupportedContentTypeFilter();

// Act
filter.OnActionExecuting(context);

// Assert
Assert.Null(context.Result);
}

[Fact]
public void OnActionExecuting_DoesNotChangeActionResult_IfModelStateIsValid()
{
// Arrange
var context = new ActionExecutingContext(
new ActionContext
{
HttpContext = new DefaultHttpContext(),
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor()
},
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());

var filter = new UnsupportedContentTypeFilter();

// Act
filter.OnActionExecuting(context);

// Assert
Assert.Null(context.Result);
}

[Fact]
public void OnActionExecuting_DoesNotChangeActionResult_IfOtherExceptionsAreFoundOnModelState()
{
// Arrange
var context = new ActionExecutingContext(
new ActionContext
{
HttpContext = new DefaultHttpContext(),
RouteData = new RouteData(),
ActionDescriptor = new ActionDescriptor()
},
new List<IFilterMetadata>(),
new Dictionary<string, object>(),
new object());

var modelMetadata = new EmptyModelMetadataProvider()
.GetMetadataForType(typeof(int));

context.ModelState.AddModelError(
"person.body",
new Exception("error"),
modelMetadata);

var filter = new UnsupportedContentTypeFilter();

// Act
filter.OnActionExecuting(context);

// Assert
Assert.Null(context.Result);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public async Task NullFormatterError_AddedToModelState()
// Key is empty because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
var errorMessage = Assert.Single(entry.Value.Errors).Exception.Message;
Assert.Equal("Unsupported content type 'text/xyz'.", errorMessage);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public async Task NoRequestContentType_SelectsActionWithoutConstraint()

// Act
var response = await Client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(await response.Content.ReadAsStringAsync());
var body = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Null(product);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("CreateProduct_Product_Text", body);
}

[Fact]
Expand All @@ -49,11 +49,11 @@ public async Task NoRequestContentType_Selects_IfASingleActionWithConstraintIsPr

// Act
var response = await Client.SendAsync(request);
var product = JsonConvert.DeserializeObject<Product>(await response.Content.ReadAsStringAsync());
var body = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
Assert.Null(product);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("ConsumesAttribute_PassThrough_Product_Json", body);
}

[Theory]
Expand Down
5 changes: 2 additions & 3 deletions test/Microsoft.AspNetCore.Mvc.FunctionalTests/FiltersTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,16 +552,15 @@ public async Task ResourceFilter_ChangesInputFormatters_XmlDenied()
"<SampleInt>10</SampleInt>" +
"</DummyClass>";

// There's nothing that can deserialize the body, so the result contains the default value.
// There's nothing that can deserialize the body, so the result is UnsupportedMediaType.
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Json");
request.Content = new StringContent(input, Encoding.UTF8, "application/xml");

// Act
var response = await Client.SendAsync(request);

// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("\"0\"", await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,47 +59,6 @@ public async Task JsonInputFormatter_IsSelectedForJsonRequest(string requestCont
Assert.Equal(sampleInputInt.ToString(), await response.Content.ReadAsStringAsync());
}

[Theory]
[InlineData("", true)]
[InlineData(null, true)]
[InlineData("invalid", true)]
[InlineData("application/custom", true)]
[InlineData("image/jpg", true)]
[InlineData("", false)]
[InlineData(null, false)]
[InlineData("invalid", false)]
[InlineData("application/custom", false)]
[InlineData("image/jpg", false)]
public async Task ModelStateErrorValidation_NoInputFormatterFound_ForGivenContentType(
string requestContentType,
bool filterHandlesModelStateError)
{
// Arrange
var actionName = filterHandlesModelStateError ? "ActionFilterHandlesError" : "ActionHandlesError";
var expectedSource = filterHandlesModelStateError ? "filter" : "action";
var input = "{\"SampleInt\":10}";
var content = new StringContent(input);
content.Headers.Clear();
content.Headers.TryAddWithoutValidation("Content-Type", requestContentType);

// Act
var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/InputFormatter/" + actionName);
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json"));
request.Content = content;
var response = await Client.SendAsync(request);

var responseBody = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<FormatterWebSite.ErrorInfo>(responseBody);

// Assert
Assert.Equal(1, result.Errors.Count);
Assert.Equal("Unsupported content type '" + requestContentType + "'.",
result.Errors[0]);
Assert.Equal(actionName, result.ActionName);
Assert.Equal("dummy", result.ParameterName);
Assert.Equal(expectedSource, result.Source);
}

[Theory]
[InlineData("application/json", "{\"SampleInt\":10}", 10)]
[InlineData("application/json", "{}", 0)]
Expand Down Expand Up @@ -154,13 +113,11 @@ public async Task JsonInputFormatter_ReadsPrimitiveTypes()
Assert.Equal(expected, responseBody);
}

[Theory]
[InlineData("{\"SampleInt\":10}")]
[InlineData("{}")]
[InlineData("")]
public async Task JsonInputFormatter_IsModelStateInvalid_ForEmptyContentType(string jsonInput)
[Fact]
public async Task JsonInputFormatter_Returns415UnsupportedMediaType_ForEmptyContentType()
{
// Arrange
var jsonInput = "{\"SampleInt\":10}";
var content = new StringContent(jsonInput, Encoding.UTF8, "application/json");
content.Headers.Clear();

Expand All @@ -169,7 +126,7 @@ public async Task JsonInputFormatter_IsModelStateInvalid_ForEmptyContentType(str
var responseBody = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
}

[Theory]
Expand Down Expand Up @@ -225,7 +182,7 @@ public async Task CustomFormatter_NotSelected_ForUnsupportedContentType(string c
var responseBody = await response.Content.ReadAsStringAsync();

// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
}
}
}
Loading

0 comments on commit 7cbb263

Please sign in to comment.