Skip to content

Commit c76df96

Browse files
authored
Treat FormatExceptions and OverflowExceptions to be treated as model state errors (#15035)
In SystemTextJsonInputFormatter and NewtonsoftJsonInputFormatter, suppress FormatExceptions and OverflowExceptions and instead report them as model state errors. This makes the behavior more consistent between these formatters, model binders, and the XML formatters Fixes #14504
1 parent 1b2c443 commit c76df96

File tree

5 files changed

+190
-10
lines changed

5 files changed

+190
-10
lines changed

src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonInputFormatter.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Http;
1010
using Microsoft.AspNetCore.Mvc.Formatters.Json;
11-
using Microsoft.AspNetCore.Mvc.ModelBinding;
1211
using Microsoft.Extensions.Logging;
1312

1413
namespace Microsoft.AspNetCore.Mvc.Formatters
@@ -87,6 +86,16 @@ public sealed override async Task<InputFormatterResult> ReadRequestBodyAsync(
8786

8887
return InputFormatterResult.Failure();
8988
}
89+
catch (Exception exception) when (exception is FormatException || exception is OverflowException)
90+
{
91+
// The code in System.Text.Json never throws these exceptions. However a custom converter could produce these errors for instance when
92+
// parsing a value. These error messages are considered safe to report to users using ModelState.
93+
94+
context.ModelState.TryAddModelError(string.Empty, exception, context.Metadata);
95+
Log.JsonInputException(_logger, exception);
96+
97+
return InputFormatterResult.Failure();
98+
}
9099
finally
91100
{
92101
if (inputStream is TranscodingReadStream transcoding)

src/Mvc/Mvc.Core/test/Formatters/JsonInputFormatterTestBase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ public virtual async Task ReadAsync_InvalidArray_AddsOverflowErrorsToModelState(
334334
// Assert
335335
Assert.True(result.HasError, "Model should have produced an error!");
336336
Assert.Collection(formatterContext.ModelState.OrderBy(k => k.Key),
337-
kvp => {
337+
kvp =>
338+
{
338339
Assert.Equal(expectedValue, kvp.Key);
339340
});
340341
}

src/Mvc/Mvc.Core/test/Formatters/SystemTextJsonInputFormatterTest.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Text;
8+
using System.Text.Json;
9+
using System.Text.Json.Serialization;
810
using System.Threading.Tasks;
911
using Microsoft.Extensions.Logging;
1012
using Xunit;
@@ -81,6 +83,48 @@ public async Task ReadAsync_SingleError()
8183
});
8284
}
8385

86+
[Fact]
87+
public async Task ReadAsync_DoesNotThrowFormatException()
88+
{
89+
// Arrange
90+
var formatter = GetInputFormatter();
91+
92+
var contentBytes = Encoding.UTF8.GetBytes("{\"dateValue\":\"not-a-date\"}");
93+
var httpContext = GetHttpContext(contentBytes);
94+
95+
var formatterContext = CreateInputFormatterContext(typeof(TypeWithBadConverters), httpContext);
96+
97+
// Act
98+
await formatter.ReadAsync(formatterContext);
99+
100+
Assert.False(formatterContext.ModelState.IsValid);
101+
var kvp = Assert.Single(formatterContext.ModelState);
102+
Assert.Empty(kvp.Key);
103+
var error = Assert.Single(kvp.Value.Errors);
104+
Assert.Equal("The supplied value is invalid.", error.ErrorMessage);
105+
}
106+
107+
[Fact]
108+
public async Task ReadAsync_DoesNotThrowOverflowException()
109+
{
110+
// Arrange
111+
var formatter = GetInputFormatter();
112+
113+
var contentBytes = Encoding.UTF8.GetBytes("{\"shortValue\":\"32768\"}");
114+
var httpContext = GetHttpContext(contentBytes);
115+
116+
var formatterContext = CreateInputFormatterContext(typeof(TypeWithBadConverters), httpContext);
117+
118+
// Act
119+
await formatter.ReadAsync(formatterContext);
120+
121+
Assert.False(formatterContext.ModelState.IsValid);
122+
var kvp = Assert.Single(formatterContext.ModelState);
123+
Assert.Empty(kvp.Key);
124+
var error = Assert.Single(kvp.Value.Errors);
125+
Assert.Equal("The supplied value is invalid.", error.ErrorMessage);
126+
}
127+
84128
protected override TextInputFormatter GetInputFormatter()
85129
{
86130
return new SystemTextJsonInputFormatter(new JsonOptions(), LoggerFactory.CreateLogger<SystemTextJsonInputFormatter>());
@@ -99,5 +143,40 @@ protected override TextInputFormatter GetInputFormatter()
99143
internal override string ReadAsync_InvalidComplexArray_AddsOverflowErrorsToModelState_Expected => "$[1].Small";
100144

101145
internal override string ReadAsync_ComplexPoco_Expected => "$.Person.Numbers[2]";
146+
147+
private class TypeWithBadConverters
148+
{
149+
[JsonConverter(typeof(DateTimeConverter))]
150+
public DateTime DateValue { get; set; }
151+
152+
[JsonConverter(typeof(ShortConverter))]
153+
public short ShortValue { get; set; }
154+
}
155+
156+
private class ShortConverter : JsonConverter<short>
157+
{
158+
public override short Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
159+
{
160+
return short.Parse(reader.GetString());
161+
}
162+
163+
public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOptions options)
164+
{
165+
throw new NotImplementedException();
166+
}
167+
}
168+
169+
private class DateTimeConverter : JsonConverter<DateTime>
170+
{
171+
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
172+
{
173+
return DateTime.Parse(reader.GetString());
174+
}
175+
176+
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
177+
{
178+
throw new NotImplementedException();
179+
}
180+
}
102181
}
103182
}

src/Mvc/Mvc.NewtonsoftJson/src/NewtonsoftJsonInputFormatter.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,15 @@ public override async Task<InputFormatterResult> ReadRequestBodyAsync(
196196
}
197197
}
198198

199-
if (!(exception is JsonException || exception is OverflowException))
199+
if (!(exception is JsonException || exception is OverflowException || exception is FormatException))
200200
{
201+
// At this point we've already recorded all exceptions as an entry in the ModelStateDictionary.
202+
// We only need to rethrow an exception if we believe it needs to be handled by something further up
203+
// the stack.
204+
// JsonException, OverflowException, and FormatException are assumed to be only encountered when
205+
// parsing the JSON and are consequently "safe" to be exposed as part of ModelState. Everything else
206+
// needs to be rethrown.
207+
201208
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
202209
exceptionDispatchInfo.Throw();
203210
}

src/Mvc/Mvc.NewtonsoftJson/test/NewtonsoftJsonInputFormatterTest.cs

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@
1414
using Microsoft.Extensions.ObjectPool;
1515
using Moq;
1616
using Newtonsoft.Json;
17+
using Newtonsoft.Json.Converters;
1718
using Newtonsoft.Json.Serialization;
1819
using Xunit;
1920

2021
namespace Microsoft.AspNetCore.Mvc.Formatters
2122
{
2223
public class NewtonsoftJsonInputFormatterTest : JsonInputFormatterTestBase
2324
{
24-
private static readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider();
25-
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings();
25+
private readonly ObjectPoolProvider _objectPoolProvider = new DefaultObjectPoolProvider();
26+
private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings();
2627

2728
[Fact]
2829
public async Task Constructor_BuffersRequestBody_UsingDefaultOptions()
@@ -144,7 +145,7 @@ public void Constructor_UsesSerializerSettings()
144145
var serializerSettings = new JsonSerializerSettings();
145146

146147
// Act
147-
var formatter = new TestableJsonInputFormatter(serializerSettings);
148+
var formatter = new TestableJsonInputFormatter(serializerSettings, _objectPoolProvider);
148149

149150
// Assert
150151
Assert.Same(serializerSettings, formatter.SerializerSettings);
@@ -185,7 +186,7 @@ public void CreateJsonSerializer_UsesJsonSerializerSettings()
185186
MaxDepth = 2,
186187
DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
187188
};
188-
var formatter = new TestableJsonInputFormatter(settings);
189+
var formatter = new TestableJsonInputFormatter(settings, _objectPoolProvider);
189190

190191
// Act
191192
var actual = formatter.CreateJsonSerializer(null);
@@ -304,7 +305,7 @@ public async Task ReadAsync_DoNotAllowInputFormatterExceptionMessages_DoesNotWra
304305
}
305306

306307
[Fact]
307-
public async Task ReadAsync_lowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
308+
public async Task ReadAsync_AllowInputFormatterExceptionMessages_DoesNotWrapJsonInputExceptions()
308309
{
309310
// Arrange
310311
var formatter = new NewtonsoftJsonInputFormatter(
@@ -336,10 +337,72 @@ public async Task ReadAsync_lowInputFormatterExceptionMessages_DoesNotWrapJsonIn
336337
Assert.NotEmpty(modelError.ErrorMessage);
337338
}
338339

340+
[Fact]
341+
public async Task ReadAsync_DoesNotRethrowFormatExceptions()
342+
{
343+
// Arrange
344+
_serializerSettings.Converters.Add(new IsoDateTimeConverter());
345+
346+
var formatter = new NewtonsoftJsonInputFormatter(
347+
GetLogger(),
348+
_serializerSettings,
349+
ArrayPool<char>.Shared,
350+
_objectPoolProvider,
351+
new MvcOptions(),
352+
new MvcNewtonsoftJsonOptions());
353+
354+
var contentBytes = Encoding.UTF8.GetBytes("{\"dateValue\":\"not-a-date\"}");
355+
var httpContext = GetHttpContext(contentBytes);
356+
357+
var formatterContext = CreateInputFormatterContext(typeof(TypeWithPrimitives), httpContext);
358+
359+
// Act
360+
var result = await formatter.ReadAsync(formatterContext);
361+
362+
// Assert
363+
Assert.True(result.HasError);
364+
Assert.False(formatterContext.ModelState.IsValid);
365+
366+
var modelError = Assert.Single(formatterContext.ModelState["dateValue"].Errors);
367+
Assert.Null(modelError.Exception);
368+
Assert.Equal("The supplied value is invalid.", modelError.ErrorMessage);
369+
}
370+
371+
[Fact]
372+
public async Task ReadAsync_DoesNotRethrowOverflowExceptions()
373+
{
374+
// Arrange
375+
_serializerSettings.Converters.Add(new IsoDateTimeConverter());
376+
377+
var formatter = new NewtonsoftJsonInputFormatter(
378+
GetLogger(),
379+
_serializerSettings,
380+
ArrayPool<char>.Shared,
381+
_objectPoolProvider,
382+
new MvcOptions(),
383+
new MvcNewtonsoftJsonOptions());
384+
385+
var contentBytes = Encoding.UTF8.GetBytes("{\"shortValue\":\"32768\"}");
386+
var httpContext = GetHttpContext(contentBytes);
387+
388+
var formatterContext = CreateInputFormatterContext(typeof(TypeWithPrimitives), httpContext);
389+
390+
// Act
391+
var result = await formatter.ReadAsync(formatterContext);
392+
393+
// Assert
394+
Assert.True(result.HasError);
395+
Assert.False(formatterContext.ModelState.IsValid);
396+
397+
var modelError = Assert.Single(formatterContext.ModelState["shortValue"].Errors);
398+
Assert.Null(modelError.Exception);
399+
Assert.Equal("The supplied value is invalid.", modelError.ErrorMessage);
400+
}
401+
339402
private class TestableJsonInputFormatter : NewtonsoftJsonInputFormatter
340403
{
341-
public TestableJsonInputFormatter(JsonSerializerSettings settings)
342-
: base(GetLogger(), settings, ArrayPool<char>.Shared, _objectPoolProvider, new MvcOptions(), new MvcNewtonsoftJsonOptions())
404+
public TestableJsonInputFormatter(JsonSerializerSettings settings, ObjectPoolProvider objectPoolProvider)
405+
: base(GetLogger(), settings, ArrayPool<char>.Shared, objectPoolProvider, new MvcOptions(), new MvcNewtonsoftJsonOptions())
343406
{
344407
}
345408

@@ -418,5 +481,26 @@ private sealed class UserLogin
418481
[JsonProperty(Required = Required.Always)]
419482
public string Password { get; set; }
420483
}
484+
485+
public class TypeWithPrimitives
486+
{
487+
public DateTime DateValue { get; set; }
488+
489+
[JsonConverter(typeof(IncorrectShortConverter))]
490+
public short ShortValue { get; set; }
491+
}
492+
493+
private class IncorrectShortConverter : JsonConverter<short>
494+
{
495+
public override short ReadJson(JsonReader reader, Type objectType, short existingValue, bool hasExistingValue, JsonSerializer serializer)
496+
{
497+
return short.Parse(reader.Value.ToString());
498+
}
499+
500+
public override void WriteJson(JsonWriter writer, short value, JsonSerializer serializer)
501+
{
502+
throw new NotImplementedException();
503+
}
504+
}
421505
}
422506
}

0 commit comments

Comments
 (0)