diff --git a/src/Microsoft.AspNetCore.Mvc.Core/InputFormatterException.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterException.cs
similarity index 100%
rename from src/Microsoft.AspNetCore.Mvc.Core/InputFormatterException.cs
rename to src/Microsoft.AspNetCore.Mvc.Abstractions/Formatters/InputFormatterException.cs
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs
index 27a89bcb03..8dc60b9420 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs
@@ -7,6 +7,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -263,6 +264,11 @@ public bool TryAddModelError(string key, Exception exception, ModelMetadata meta
return TryAddModelError(key, errorMessage);
}
+ else if (exception is InputFormatterException && !string.IsNullOrEmpty(exception.Message))
+ {
+ // InputFormatterException is a signal that the message is safe to expose to clients
+ return TryAddModelError(key, exception.Message);
+ }
ErrorCount++;
AddModelErrorCore(key, exception);
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs
index 1f17afa385..21c3f81627 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs
@@ -170,11 +170,20 @@ public int MaxModelValidationErrors
public bool AllowBindingUndefinedValueToEnumType { get; set; }
///
- /// Gets or sets the option to determine if model binding should convert all exceptions(including ones not related to bad input)
+ /// Gets or sets the option to determine if model binding should convert all exceptions (including ones not related to bad input)
/// that occur during deserialization in s into model state errors.
/// This option applies only to custom s.
/// Default is .
///
public InputFormatterExceptionModelStatePolicy InputFormatterExceptionModelStatePolicy { get; set; }
+
+ ///
+ /// Gets or sets a flag to determine whether, if an action receives invalid JSON in
+ /// the request body, the JSON deserialization exception message should be replaced
+ /// by a generic error message in model state.
+ /// by default, meaning that clients may receive details about
+ /// why the JSON they posted is considered invalid.
+ ///
+ public bool SuppressJsonDeserializationExceptionMessagesInModelState { get; set; } = false;
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/AssemblyInfo.cs
index 18b9d45ba6..df50c7d842 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/AssemblyInfo.cs
@@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Mvc.Formatters;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Core.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
+[assembly: TypeForwardedTo(typeof(InputFormatterException))]
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs
index 91fcd8ea75..ad32ab6505 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/Internal/MvcJsonMvcOptionsSetup.cs
@@ -69,7 +69,8 @@ public void Configure(MvcOptions options)
_jsonSerializerSettings,
_charPool,
_objectPoolProvider,
- options.SuppressInputFormatterBuffering));
+ options.SuppressInputFormatterBuffering,
+ options.SuppressJsonDeserializationExceptionMessagesInModelState));
var jsonInputLogger = _loggerFactory.CreateLogger();
options.InputFormatters.Add(new JsonInputFormatter(
@@ -77,7 +78,8 @@ public void Configure(MvcOptions options)
_jsonSerializerSettings,
_charPool,
_objectPoolProvider,
- options.SuppressInputFormatterBuffering));
+ options.SuppressInputFormatterBuffering,
+ options.SuppressJsonDeserializationExceptionMessagesInModelState));
options.FormatterMappings.SetMediaTypeMappingForFormat("json", MediaTypeHeaderValue.Parse("application/json"));
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs
index 6ce44c0876..e0c5ed650a 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs
@@ -28,6 +28,7 @@ public class JsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPo
private readonly ILogger _logger;
private readonly ObjectPoolProvider _objectPoolProvider;
private readonly bool _suppressInputFormatterBuffering;
+ private readonly bool _suppressJsonDeserializationExceptionMessages;
private ObjectPool _jsonSerializerPool;
@@ -70,6 +71,32 @@ public JsonInputFormatter(
ArrayPool charPool,
ObjectPoolProvider objectPoolProvider,
bool suppressInputFormatterBuffering)
+ : this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, suppressJsonDeserializationExceptionMessages: false)
+ {
+ // This constructor by default treats JSON deserialization exceptions as safe
+ // because this is the default for applications generally
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The .
+ ///
+ /// The . Should be either the application-wide settings
+ /// () or an instance
+ /// initially returned.
+ ///
+ /// The .
+ /// The .
+ /// Flag to buffer entire request body before deserializing it.
+ /// If , JSON deserialization exception messages will replaced by a generic message in model state.
+ public JsonInputFormatter(
+ ILogger logger,
+ JsonSerializerSettings serializerSettings,
+ ArrayPool charPool,
+ ObjectPoolProvider objectPoolProvider,
+ bool suppressInputFormatterBuffering,
+ bool suppressJsonDeserializationExceptionMessages)
{
if (logger == null)
{
@@ -96,6 +123,7 @@ public JsonInputFormatter(
_charPool = new JsonArrayPool(charPool);
_objectPoolProvider = objectPoolProvider;
_suppressInputFormatterBuffering = suppressInputFormatterBuffering;
+ _suppressJsonDeserializationExceptionMessages = suppressJsonDeserializationExceptionMessages;
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
@@ -187,7 +215,8 @@ void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs ev
}
var metadata = GetPathMetadata(context.Metadata, eventArgs.ErrorContext.Path);
- context.ModelState.TryAddModelError(key, eventArgs.ErrorContext.Error, metadata);
+ var modelStateException = WrapExceptionForModelState(eventArgs.ErrorContext.Error);
+ context.ModelState.TryAddModelError(key, modelStateException, metadata);
_logger.JsonInputException(eventArgs.ErrorContext.Error);
@@ -315,5 +344,19 @@ private ModelMetadata GetPathMetadata(ModelMetadata metadata, string path)
return metadata;
}
+
+ private Exception WrapExceptionForModelState(Exception exception)
+ {
+ // It's not known that Json.NET currently ever raises error events with exceptions
+ // other than these two types, but we're being conservative and limiting which ones
+ // we regard as having safe messages to expose to clients
+ var isJsonExceptionType =
+ exception is JsonReaderException || exception is JsonSerializationException;
+ var suppressOriginalMessage =
+ _suppressJsonDeserializationExceptionMessages || !isJsonExceptionType;
+ return suppressOriginalMessage
+ ? exception
+ : new InputFormatterException(exception.Message, exception);
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs
index b04522111b..9f1c94945b 100644
--- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs
@@ -27,7 +27,8 @@ public class JsonPatchInputFormatter : JsonInputFormatter
/// The . Should be either the application-wide settings
/// () or an instance
/// initially returned.
- /// /// The .
+ ///
+ /// The .
/// The .
public JsonPatchInputFormatter(
ILogger logger,
@@ -46,7 +47,8 @@ public JsonPatchInputFormatter(
/// The . Should be either the application-wide settings
/// () or an instance
/// initially returned.
- /// /// The .
+ ///
+ /// The .
/// The .
/// Flag to buffer entire request body before deserializing it.
public JsonPatchInputFormatter(
@@ -55,7 +57,31 @@ public JsonPatchInputFormatter(
ArrayPool charPool,
ObjectPoolProvider objectPoolProvider,
bool suppressInputFormatterBuffering)
- : base(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering)
+ : this(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, suppressJsonDeserializationExceptionMessages: false)
+ {
+ }
+
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The .
+ ///
+ /// The . Should be either the application-wide settings
+ /// () or an instance
+ /// initially returned.
+ ///
+ /// The .
+ /// The .
+ /// Flag to buffer entire request body before deserializing it.
+ /// If , JSON deserialization exception messages will replaced by a generic message in model state.
+ public JsonPatchInputFormatter(
+ ILogger logger,
+ JsonSerializerSettings serializerSettings,
+ ArrayPool charPool,
+ ObjectPoolProvider objectPoolProvider,
+ bool suppressInputFormatterBuffering,
+ bool suppressJsonDeserializationExceptionMessages)
+ : base(logger, serializerSettings, charPool, objectPoolProvider, suppressInputFormatterBuffering, suppressJsonDeserializationExceptionMessages)
{
// Clear all values and only include json-patch+json value.
SupportedMediaTypes.Clear();
diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs
index 36a7f874c8..c06668528c 100644
--- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs
@@ -2,6 +2,7 @@
// 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;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
@@ -1005,7 +1006,7 @@ public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithNo
}
[Fact]
- public void ModelStateDictionary_NoErrorMessage_ForNonFormatException()
+ public void ModelStateDictionary_NoErrorMessage_ForUnrecognizedException()
{
// Arrange
var dictionary = new ModelStateDictionary();
@@ -1021,6 +1022,28 @@ public void ModelStateDictionary_NoErrorMessage_ForNonFormatException()
Assert.Empty(error.ErrorMessage);
}
+ [Fact]
+ public void ModelStateDictionary_AddsErrorMessage_ForInputFormatterException()
+ {
+ // Arrange
+ var expectedMessage = "This is an InputFormatterException";
+ var dictionary = new ModelStateDictionary();
+
+ var bindingMetadataProvider = new DefaultBindingMetadataProvider();
+ var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
+ var provider = new DefaultModelMetadataProvider(compositeProvider, new OptionsAccessor());
+ var metadata = provider.GetMetadataForType(typeof(int));
+
+ // Act
+ dictionary.TryAddModelError("key", new InputFormatterException(expectedMessage), metadata);
+
+ // Assert
+ var entry = Assert.Single(dictionary);
+ Assert.Equal("key", entry.Key);
+ var error = Assert.Single(entry.Value.Errors);
+ Assert.Equal(expectedMessage, error.ErrorMessage);
+ }
+
[Fact]
public void ModelStateDictionary_ClearEntriesThatMatchWithKey_NonEmptyKey()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs
index addf26f1d2..84e0c5cb69 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderTests.cs
@@ -232,10 +232,9 @@ public async Task BindModel_CustomFormatter_ThrowingInputFormatterException_Adds
// Key is the empty string 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).Exception.Message;
+ var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
Assert.Equal("Bad input!!", errorMessage);
- var formatException = Assert.IsType(entry.Value.Errors[0].Exception.InnerException);
- Assert.Same(expectedFormatException, formatException);
+ Assert.Null(entry.Value.Errors[0].Exception);
}
public static TheoryData BuiltInFormattersThrowingInputFormatterException
@@ -282,9 +281,9 @@ public async Task BindModel_BuiltInXmlInputFormatters_ThrowingInputFormatterExce
// Key is the empty string 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).Exception.Message;
+ var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
Assert.Equal("An error occured while deserializing input data.", errorMessage);
- Assert.IsType(entry.Value.Errors[0].Exception);
+ Assert.Null(entry.Value.Errors[0].Exception);
}
[Theory]
@@ -319,7 +318,7 @@ public async Task BindModel_BuiltInJsonInputFormatter_ThrowingInputFormatterExce
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
- Assert.IsType(entry.Value.Errors[0].Exception);
+ Assert.NotEmpty(entry.Value.Errors[0].ErrorMessage);
}
public static TheoryData DerivedFormattersThrowingInputFormatterException
@@ -366,9 +365,9 @@ public async Task BindModel_DerivedXmlInputFormatters_AddsErrorToModelState_(
// Key is the empty string 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).Exception.Message;
+ var errorMessage = Assert.Single(entry.Value.Errors).ErrorMessage;
Assert.Equal("An error occured while deserializing input data.", errorMessage);
- Assert.IsType(entry.Value.Errors[0].Exception);
+ Assert.Null(entry.Value.Errors[0].Exception);
}
[Theory]
@@ -403,7 +402,8 @@ public async Task BindModel_DerivedJsonInputFormatter_AddsErrorToModelState(
// Key is the empty string because this was a top-level binding.
var entry = Assert.Single(bindingContext.ModelState);
Assert.Equal(string.Empty, entry.Key);
- Assert.IsType(entry.Value.Errors[0].Exception);
+ Assert.NotEmpty(entry.Value.Errors[0].ErrorMessage);
+ Assert.Null(entry.Value.Errors[0].Exception);
}
// Throwing Non-InputFormatterException
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDetailsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDetailsTest.cs
index 2fb53be244..1254add386 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDetailsTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ValidationProblemDetailsTest.cs
@@ -1,6 +1,7 @@
// 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.ModelBinding;
using Xunit;
@@ -47,5 +48,30 @@ public void Constructor_SerializesErrorsFromModelStateDictionary()
Assert.Equal(new[] { "error2", "error3" }, item.Value);
});
}
+
+ [Fact]
+ public void Constructor_SerializesErrorsFromModelStateDictionary_AddsDefaultMessage()
+ {
+ // Arrange
+ var modelStateDictionary = new ModelStateDictionary();
+ var provider = new EmptyModelMetadataProvider();
+ var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
+ modelStateDictionary.AddModelError("unsafeError",
+ new Exception("This message should not be returned to clients"),
+ metadata);
+
+ // Act
+ var problemDescription = new ValidationProblemDetails(modelStateDictionary);
+
+ // Assert
+ Assert.Equal("One or more validation errors occured.", problemDescription.Title);
+ Assert.Collection(
+ problemDescription.Errors,
+ item =>
+ {
+ Assert.Equal("unsafeError", item.Key);
+ Assert.Equal(new[] { "The input was not valid." }, item.Value);
+ });
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
index 09e5be855a..e879f4f424 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
@@ -331,7 +331,7 @@ public async Task ReadAsync_AddsModelValidationErrorsToModelState()
Assert.True(result.HasError);
Assert.Equal(
"Could not convert string to decimal: not-an-age. Path 'Age', line 1, position 39.",
- modelState["Age"].Errors[0].Exception.Message);
+ modelState["Age"].Errors[0].ErrorMessage);
}
[Fact]
@@ -392,7 +392,7 @@ public async Task ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState()
Assert.True(result.HasError);
Assert.Equal(
"Error converting value 300 to type 'System.Byte'. Path '[1].Small', line 1, position 59.",
- modelState["names[1].Small"].Errors[0].Exception.Message);
+ modelState["names[1].Small"].Errors[0].ErrorMessage);
}
[Fact]
@@ -508,7 +508,7 @@ public async Task CustomSerializerSettingsObject_TakesEffect()
Assert.True(result.HasError);
Assert.False(modelState.IsValid);
- var modelErrorMessage = modelState.Values.First().Errors[0].Exception.Message;
+ var modelErrorMessage = modelState.Values.First().Errors[0].ErrorMessage;
Assert.Contains("Required property 'Password' not found in JSON", modelErrorMessage);
}
@@ -533,6 +533,81 @@ public void CreateJsonSerializer_UsesJsonSerializerSettings()
Assert.Equal(settings.DateTimeZoneHandling, actual.DateTimeZoneHandling);
}
+ [Theory]
+ [InlineData("{", "", "Unexpected end when reading JSON. Path '', line 1, position 1.")]
+ [InlineData("{\"a\":{\"b\"}}", "a", "Invalid character after parsing property name. Expected ':' but got: }. Path 'a', line 1, position 9.")]
+ [InlineData("{\"age\":\"x\"}", "age", "Could not convert string to decimal: x. Path 'age', line 1, position 10.")]
+ [InlineData("{\"login\":1}", "login", "Error converting value 1 to type 'Microsoft.AspNetCore.Mvc.Formatters.JsonInputFormatterTest+UserLogin'. Path 'login', line 1, position 10.")]
+ [InlineData("{\"login\":{\"username\":\"somevalue\"}}", "login", "Required property 'Password' not found in JSON. Path 'login', line 1, position 33.")]
+ public async Task ReadAsync_RegistersJsonInputExceptionsAsInputFormatterException(
+ string content,
+ string modelStateKey,
+ string expectedMessage)
+ {
+ // Arrange
+ var logger = GetLogger();
+ var formatter =
+ new JsonInputFormatter(logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider);
+ var contentBytes = Encoding.UTF8.GetBytes(content);
+
+ var modelState = new ModelStateDictionary();
+ var httpContext = GetHttpContext(contentBytes);
+ var provider = new EmptyModelMetadataProvider();
+ var metadata = provider.GetMetadataForType(typeof(User));
+ var context = new InputFormatterContext(
+ httpContext,
+ modelName: string.Empty,
+ modelState: modelState,
+ metadata: metadata,
+ readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
+
+ // Act
+ var result = await formatter.ReadAsync(context);
+
+ // Assert
+ Assert.True(result.HasError);
+ Assert.True(!modelState.IsValid);
+ Assert.True(modelState.ContainsKey(modelStateKey));
+
+ var modelError = modelState[modelStateKey].Errors.Single();
+ Assert.Equal(expectedMessage, modelError.ErrorMessage);
+ }
+
+ [Fact]
+ public async Task ReadAsync_WhenSuppressJsonDeserializationExceptionMessagesIsTrue_DoesNotWrapJsonInputExceptions()
+ {
+ // Arrange
+ var logger = GetLogger();
+ var formatter = new JsonInputFormatter(
+ logger, _serializerSettings, ArrayPool.Shared, _objectPoolProvider,
+ suppressInputFormatterBuffering: false, suppressJsonDeserializationExceptionMessages: true);
+ var contentBytes = Encoding.UTF8.GetBytes("{");
+ var modelStateKey = string.Empty;
+
+ var modelState = new ModelStateDictionary();
+ var httpContext = GetHttpContext(contentBytes);
+ var provider = new EmptyModelMetadataProvider();
+ var metadata = provider.GetMetadataForType(typeof(User));
+ var context = new InputFormatterContext(
+ httpContext,
+ modelName: string.Empty,
+ modelState: modelState,
+ metadata: metadata,
+ readerFactory: new TestHttpRequestStreamReaderFactory().CreateReader);
+
+ // Act
+ var result = await formatter.ReadAsync(context);
+
+ // Assert
+ Assert.True(result.HasError);
+ Assert.True(!modelState.IsValid);
+ Assert.True(modelState.ContainsKey(modelStateKey));
+
+ var modelError = modelState[modelStateKey].Errors.Single();
+ Assert.IsNotType(modelError.Exception);
+ Assert.Empty(modelError.ErrorMessage);
+ }
+
private class TestableJsonInputFormatter : JsonInputFormatter
{
public TestableJsonInputFormatter(JsonSerializerSettings settings)
@@ -609,6 +684,8 @@ private sealed class User
public decimal Age { get; set; }
public byte Small { get; set; }
+
+ public UserLogin Login { get; set; }
}
private sealed class UserLogin
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs
index cf0fece88d..202d008019 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonPatchInputFormatterTest.cs
@@ -271,7 +271,7 @@ public async Task JsonPatchInputFormatter_ReturnsModelStateErrors_InvalidModelTy
// Assert
Assert.True(result.HasError);
- Assert.Contains(exceptionMessage, modelState[""].Errors[0].Exception.Message);
+ Assert.Contains(exceptionMessage, modelState[""].Errors[0].ErrorMessage);
}
private static ILogger GetLogger()
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs
index 7c91c32e1c..0b6c52aab2 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs
@@ -97,6 +97,21 @@ public async Task JsonInputFormatter_ReturnsBadRequest_ForEmptyRequestBody(
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
+ [Fact]
+ public async Task JsonInputFormatter_SuppliedJsonDeserializationErrorMessage()
+ {
+ // Arrange
+ var content = new StringContent("{", Encoding.UTF8, "application/json");
+
+ // Act
+ var response = await Client.PostAsync("http://localhost/JsonFormatter/ReturnInput/", content);
+ var responseBody = await response.Content.ReadAsStringAsync();
+
+ // Assert
+ Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
+ Assert.Equal("{\"\":[\"Unexpected end when reading JSON. Path '', line 1, position 1.\"]}", responseBody);
+ }
+
[Theory]
[InlineData("\"I'm a JSON string!\"")]
[InlineData("true")]
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs
index a11073b0b4..ab36a45d93 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlDataContractSerializerInputFormatterTest.cs
@@ -46,11 +46,7 @@ public async Task ThrowsOnInvalidInput_AndAddsToModelState()
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync();
- Assert.Contains(
- string.Format(
- "There was an error deserializing the object of type {0}.",
- typeof(DummyClass).FullName),
- data);
+ Assert.Contains("An error occured while deserializing input data.", data);
}
[Fact]
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs
index 0dfa1016ce..0589d349d4 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/XmlSerializerInputFormatterTests.cs
@@ -52,7 +52,7 @@ public async Task ThrowsOnInvalidInput_AndAddsToModelState()
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var data = await response.Content.ReadAsStringAsync();
- Assert.Contains("There is an error in XML document", data);
+ Assert.Contains("An error occured while deserializing input data.", data);
}
}
}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs
index 33b287aa97..788b0d8da8 100644
--- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/BodyValidationIntegrationTests.cs
@@ -485,11 +485,11 @@ public async Task FromBodyAndRequiredOnValueTypeProperty_EmptyBody_JsonFormatter
Assert.Null(entry.Value.AttemptedValue);
Assert.Null(entry.Value.RawValue);
var error = Assert.Single(entry.Value.Errors);
- Assert.NotNull(error.Exception);
+ Assert.Null(error.Exception);
// Json.NET currently throws an exception starting with "No JSON content found and type 'System.Int32' is
// not nullable." but do not tie test to a particular Json.NET build.
- Assert.NotEmpty(error.Exception.Message);
+ Assert.NotEmpty(error.ErrorMessage);
}
private class Person5
@@ -586,11 +586,11 @@ public async Task FromBodyWithInvalidPropertyData_JsonFormatterAddsModelError()
Assert.Null(state.AttemptedValue);
Assert.Null(state.RawValue);
var error = Assert.Single(state.Errors);
- Assert.NotNull(error.Exception);
+ Assert.Null(error.Exception);
// Json.NET currently throws an Exception with a Message starting with "Could not convert string to
// integer: not a number." but do not tie test to a particular Json.NET build.
- Assert.NotEmpty(error.Exception.Message);
+ Assert.NotEmpty(error.ErrorMessage);
}
[Theory]
diff --git a/test/WebSites/FormatterWebSite/Controllers/JsonFormatterController.cs b/test/WebSites/FormatterWebSite/Controllers/JsonFormatterController.cs
index 4fdafc2f66..e8e3f58d7f 100644
--- a/test/WebSites/FormatterWebSite/Controllers/JsonFormatterController.cs
+++ b/test/WebSites/FormatterWebSite/Controllers/JsonFormatterController.cs
@@ -47,7 +47,7 @@ public IActionResult ReturnInput([FromBody]DummyClass dummyObject)
{
if (!ModelState.IsValid)
{
- return BadRequest();
+ return BadRequest(ModelState);
}
return Content(dummyObject.SampleInt.ToString());