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());