/
NewtonsoftJsonInputFormatter.cs
443 lines (394 loc) · 18.8 KB
/
NewtonsoftJsonInputFormatter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Globalization;
using System.Runtime.ExceptionServices;
using System.Text;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.NewtonsoftJson;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc.Formatters;
/// <summary>
/// A <see cref="TextInputFormatter"/> for JSON content.
/// </summary>
public partial class NewtonsoftJsonInputFormatter : TextInputFormatter, IInputFormatterExceptionPolicy
{
private readonly IArrayPool<char> _charPool;
private readonly ILogger _logger;
private readonly ObjectPoolProvider _objectPoolProvider;
private readonly MvcOptions _options;
private readonly MvcNewtonsoftJsonOptions _jsonOptions;
private ObjectPool<JsonSerializer>? _jsonSerializerPool;
/// <summary>
/// Initializes a new instance of <see cref="NewtonsoftJsonInputFormatter"/>.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="serializerSettings">
/// The <see cref="JsonSerializerSettings"/>. Should be either the application-wide settings
/// (<see cref="MvcNewtonsoftJsonOptions.SerializerSettings"/>) or an instance
/// <see cref="JsonSerializerSettingsProvider.CreateSerializerSettings"/> initially returned.
/// </param>
/// <param name="charPool">The <see cref="ArrayPool{Char}"/>.</param>
/// <param name="objectPoolProvider">The <see cref="ObjectPoolProvider"/>.</param>
/// <param name="options">The <see cref="MvcOptions"/>.</param>
/// <param name="jsonOptions">The <see cref="MvcNewtonsoftJsonOptions"/>.</param>
public NewtonsoftJsonInputFormatter(
ILogger logger,
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool,
ObjectPoolProvider objectPoolProvider,
MvcOptions options,
MvcNewtonsoftJsonOptions jsonOptions)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(serializerSettings);
ArgumentNullException.ThrowIfNull(charPool);
ArgumentNullException.ThrowIfNull(objectPoolProvider);
_logger = logger;
SerializerSettings = serializerSettings;
_charPool = new JsonArrayPool<char>(charPool);
_objectPoolProvider = objectPoolProvider;
_options = options;
_jsonOptions = jsonOptions;
SupportedEncodings.Add(UTF8EncodingWithoutBOM);
SupportedEncodings.Add(UTF16EncodingLittleEndian);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextJson);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyJsonSyntax);
}
/// <inheritdoc />
public virtual InputFormatterExceptionPolicy ExceptionPolicy
{
get
{
if (GetType() == typeof(NewtonsoftJsonInputFormatter))
{
return InputFormatterExceptionPolicy.MalformedInputExceptions;
}
return InputFormatterExceptionPolicy.AllExceptions;
}
}
/// <summary>
/// Gets the <see cref="JsonSerializerSettings"/> used to configure the <see cref="JsonSerializer"/>.
/// </summary>
/// <remarks>
/// Any modifications to the <see cref="JsonSerializerSettings"/> object after this
/// <see cref="NewtonsoftJsonInputFormatter"/> has been used will have no effect.
/// </remarks>
protected JsonSerializerSettings SerializerSettings { get; }
/// <inheritdoc />
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context,
Encoding encoding)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(encoding);
var httpContext = context.HttpContext;
var request = httpContext.Request;
var suppressInputFormatterBuffering = _options.SuppressInputFormatterBuffering;
var readStream = request.Body;
var disposeReadStream = false;
if (readStream.CanSeek)
{
// The most common way of getting here is the user has request buffering on.
// However, request buffering isn't eager, and consequently it will peform pass-thru synchronous
// reads as part of the deserialization.
// To avoid this, drain and reset the stream.
var position = request.Body.Position;
await readStream.DrainAsync(CancellationToken.None);
readStream.Position = position;
}
else if (!suppressInputFormatterBuffering)
{
// JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
// read everything into a buffer, and then seek back to the beginning.
var memoryThreshold = _jsonOptions.InputFormatterMemoryBufferThreshold;
var contentLength = request.ContentLength.GetValueOrDefault();
if (contentLength > 0 && contentLength < memoryThreshold)
{
// If the Content-Length is known and is smaller than the default buffer size, use it.
memoryThreshold = (int)contentLength;
}
readStream = new FileBufferingReadStream(request.Body, memoryThreshold);
// Ensure the file buffer stream is always disposed at the end of a request.
httpContext.Response.RegisterForDispose(readStream);
await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
}
var successful = true;
Exception? exception = null;
object? model;
using (var streamReader = context.ReaderFactory(readStream, encoding))
{
using var jsonReader = new JsonTextReader(streamReader);
jsonReader.ArrayPool = _charPool;
jsonReader.CloseInput = false;
var type = context.ModelType;
var jsonSerializer = CreateJsonSerializer(context);
jsonSerializer.Error += ErrorHandler;
if (_jsonOptions.ReadJsonWithRequestCulture)
{
jsonSerializer.Culture = CultureInfo.CurrentCulture;
}
try
{
model = jsonSerializer.Deserialize(jsonReader, type);
}
finally
{
// Clean up the error handler since CreateJsonSerializer() pools instances.
jsonSerializer.Error -= ErrorHandler;
ReleaseJsonSerializer(jsonSerializer);
if (disposeReadStream)
{
await readStream.DisposeAsync();
}
}
}
if (successful)
{
if (model == null && !context.TreatEmptyInputAsDefaultValue)
{
// Some nonempty inputs might deserialize as null, for example whitespace,
// or the JSON-encoded value "null". The upstream BodyModelBinder needs to
// be notified that we don't regard this as a real input so it can register
// a model binding error.
return InputFormatterResult.NoValue();
}
else
{
return InputFormatterResult.Success(model);
}
}
if (exception is not null && exception is not (JsonException or OverflowException or FormatException))
{
// At this point we've already recorded all exceptions as an entry in the ModelStateDictionary.
// We only need to rethrow an exception if we believe it needs to be handled by something further up
// the stack.
// JsonException, OverflowException, and FormatException are assumed to be only encountered when
// parsing the JSON and are consequently "safe" to be exposed as part of ModelState. Everything else
// needs to be rethrown.
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
exceptionDispatchInfo.Throw();
}
return InputFormatterResult.Failure();
void ErrorHandler(object? sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs)
{
// Skipping error, if it's already marked as handled
// This allows user code to implement its own error handling
if (eventArgs.ErrorContext.Handled)
{
return;
}
successful = false;
// The following addMember logic is intended to append the names of missing required properties to the
// ModelStateDictionary key. Normally, just the ModelName and ErrorContext.Path is used for this key,
// but ErrorContext.Path does not include the missing required property name like we want it to.
// For example, given the following class and input missing the required "Name" property:
//
// class Person
// {
// [JsonProperty(Required = Required.Always)]
// public string Name { get; set; }
// }
//
// We will see the following ErrorContext:
//
// Error {"Required property 'Name' not found in JSON. Path 'Person'..."} System.Exception {Newtonsoft.Json.JsonSerializationException}
// Member "Name" object {string}
// Path "Person" string
//
// So we update the path used for the ModelStateDictionary key to be "Person.Name" instead of just "Person".
// See https://github.com/aspnet/Mvc/issues/8509
var path = eventArgs.ErrorContext.Path;
var member = eventArgs.ErrorContext.Member as string;
// There are some deserialization exceptions that include the member in the path but not at the end.
// For example, given the following classes and invalid input like { "b": { "c": { "d": abc } } }:
//
// class A
// {
// public B B { get; set; }
// }
// class B
// {
// public C C { get; set; }
// }
// class C
// {
// public string D { get; set; }
// }
//
// We will see the following ErrorContext:
//
// Error {"Unexpected character encountered while parsing value: b. Path 'b.c.d'..."} System.Exception {Newtonsoft.Json.JsonReaderException}
// Member "c" object {string}
// Path "b.c.d" string
//
// Notice that Member "c" is in the middle of the Path "b.c.d". The error handler gets invoked for each level of nesting.
// null, "b", "c" and "d" are each a Member in different ErrorContexts all reporting the same parsing error.
//
// The parsing error is reported as a JsonReaderException instead of as a JsonSerializationException like
// for missing required properties. We use the exception type to filter out these errors and keep the path used
// for the ModelStateDictionary key as "b.c.d" instead of "b.c.d.c"
// See https://github.com/dotnet/aspnetcore/issues/33451
var addMember = !string.IsNullOrEmpty(member) && eventArgs.ErrorContext.Error is JsonSerializationException;
// There are still JsonSerilizationExceptions that set ErrorContext.Member but include it at the
// end of ErrorContext.Path already. The following logic attempts to filter these out.
if (addMember)
{
// Path.Member case (path.Length < member.Length) needs no further checks.
if (path.Length == member!.Length)
{
// Add Member in Path.Member case but not for Path.Path.
addMember = !string.Equals(path, member, StringComparison.Ordinal);
}
else if (path.Length > member.Length)
{
// Finally, check whether Path already ends or starts with Member.
if (member[0] == '[')
{
addMember = !path.EndsWith(member, StringComparison.Ordinal);
}
else
{
addMember = !path.EndsWith($".{member}", StringComparison.Ordinal)
&& !path.EndsWith($"['{member}']", StringComparison.Ordinal)
&& !path.EndsWith($"[{member}]", StringComparison.Ordinal);
}
}
}
if (addMember)
{
path = ModelNames.CreatePropertyModelName(path, member);
}
// Handle path combinations such as ""+"Property", "Parent"+"Property", or "Parent"+"[12]".
var key = ModelNames.CreatePropertyModelName(context.ModelName, path);
exception = eventArgs.ErrorContext.Error;
var metadata = GetPathMetadata(context.Metadata, path);
var modelStateException = WrapExceptionForModelState(exception);
context.ModelState.TryAddModelError(key, modelStateException, metadata);
Log.JsonInputException(_logger, exception);
// Error must always be marked as handled
// Failure to do so can cause the exception to be rethrown at every recursive level and
// overflow the stack for x64 CLR processes
eventArgs.ErrorContext.Handled = true;
}
}
/// <summary>
/// Called during deserialization to get the <see cref="JsonSerializer"/>. The formatter context
/// that is passed gives an ability to create serializer specific to the context.
/// </summary>
/// <returns>The <see cref="JsonSerializer"/> used during deserialization.</returns>
/// <remarks>
/// This method works in tandem with <see cref="ReleaseJsonSerializer(JsonSerializer)"/> to
/// manage the lifetimes of <see cref="JsonSerializer"/> instances.
/// </remarks>
protected virtual JsonSerializer CreateJsonSerializer()
{
if (_jsonSerializerPool == null)
{
_jsonSerializerPool = _objectPoolProvider.Create(new JsonSerializerObjectPolicy(SerializerSettings));
}
return _jsonSerializerPool.Get();
}
/// <summary>
/// Called during deserialization to get the <see cref="JsonSerializer"/>. The formatter context
/// that is passed gives an ability to create serializer specific to the context.
/// </summary>
/// <param name="context">A context object used by an input formatter for deserializing the request body into an object.</param>
/// <returns>The <see cref="JsonSerializer"/> used during deserialization.</returns>
/// <remarks>
/// This method works in tandem with <see cref="ReleaseJsonSerializer(JsonSerializer)"/> to
/// manage the lifetimes of <see cref="JsonSerializer"/> instances.
/// </remarks>
protected virtual JsonSerializer CreateJsonSerializer(InputFormatterContext context)
{
return CreateJsonSerializer();
}
/// <summary>
/// Releases the <paramref name="serializer"/> instance.
/// </summary>
/// <param name="serializer">The <see cref="JsonSerializer"/> to release.</param>
/// <remarks>
/// This method works in tandem with <see cref="ReleaseJsonSerializer(JsonSerializer)"/> to
/// manage the lifetimes of <see cref="JsonSerializer"/> instances.
/// </remarks>
protected virtual void ReleaseJsonSerializer(JsonSerializer serializer)
=> _jsonSerializerPool!.Return(serializer);
private static ModelMetadata GetPathMetadata(ModelMetadata metadata, string path)
{
var index = 0;
while (index >= 0 && index < path.Length)
{
if (path[index] == '[')
{
// At start of "[0]".
if (metadata.ElementMetadata == null)
{
// Odd case but don't throw just because ErrorContext had an odd-looking path.
break;
}
metadata = metadata.ElementMetadata;
index = path.IndexOf(']', index);
}
else if (path[index] == '.' || path[index] == ']')
{
// Skip '.' in "prefix.property" or "[0].property" or ']' in "[0]".
index++;
}
else
{
// At start of "property", "property." or "property[0]".
var endIndex = path.AsSpan(index).IndexOfAny('.', '[');
if (endIndex < 0)
{
endIndex = path.Length;
}
else
{
endIndex += index;
}
var propertyName = path.Substring(index, endIndex - index);
var propertyMetadata = metadata.Properties[propertyName];
if (propertyMetadata is null)
{
// Odd case but don't throw just because ErrorContext had an odd-looking path.
break;
}
metadata = propertyMetadata;
index = endIndex;
}
}
return metadata;
}
private Exception WrapExceptionForModelState(Exception exception)
{
// In 2.0 and earlier we always gave a generic error message for errors that come from JSON.NET
// We only allow it in 2.1 and newer if the app opts-in.
if (!_jsonOptions.AllowInputFormatterExceptionMessages)
{
// This app is not opted-in to JSON.NET messages, return the original exception.
return 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
if (exception is JsonReaderException || exception is JsonSerializationException)
{
// InputFormatterException specifies that the message is safe to return to a client, it will
// be added to model state.
return new InputFormatterException(exception.Message, exception);
}
// Not a known exception type, so we're not going to assume that it's safe.
return exception;
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Debug, "JSON input formatter threw an exception.", EventName = "JsonInputException")]
public static partial void JsonInputException(ILogger logger, Exception exception);
}
}