[Fixes 3961] Consider InputFormatter behavior when we can't read the charset
* Broken InputFormatter into InputFormatter and TextInputFormatter * Added an exception to ModelState inside ReadAsync on TextInputFormatter when we can't find a valid encoding to read the body.
This commit is contained in:
parent
8e8de413f8
commit
fe639f028f
|
|
@ -3,14 +3,9 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
using Microsoft.AspNetCore.Mvc.Core;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
|
|
@ -19,33 +14,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider
|
public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Returns UTF8 Encoding without BOM and throws on invalid bytes.
|
|
||||||
/// </summary>
|
|
||||||
protected static readonly Encoding UTF8EncodingWithoutBOM
|
|
||||||
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes.
|
|
||||||
/// </summary>
|
|
||||||
protected static readonly Encoding UTF16EncodingLittleEndian
|
|
||||||
= new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the mutable collection of character encodings supported by
|
|
||||||
/// this <see cref="InputFormatter"/>. The encodings are
|
|
||||||
/// used when reading the data.
|
|
||||||
/// </summary>
|
|
||||||
public IList<Encoding> SupportedEncodings { get; } = new List<Encoding>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the mutable collection of media type elements supported by
|
/// Gets the mutable collection of media type elements supported by
|
||||||
/// this <see cref="InputFormatter"/>.
|
/// this <see cref="InputFormatter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection();
|
public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection();
|
||||||
|
|
||||||
protected object GetDefaultValueForType(Type modelType)
|
/// <summary>
|
||||||
|
/// Gets the default value for a given type. Used to return a default value when the body contains no content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modelType">The type of the value.</param>
|
||||||
|
/// <returns>The default value for the <paramref name="modelType"/> type.</returns>
|
||||||
|
protected virtual object GetDefaultValueForType(Type modelType)
|
||||||
{
|
{
|
||||||
|
if (modelType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(modelType));
|
||||||
|
}
|
||||||
|
|
||||||
if (modelType.GetTypeInfo().IsValueType)
|
if (modelType.GetTypeInfo().IsValueType)
|
||||||
{
|
{
|
||||||
return Activator.CreateInstance(modelType);
|
return Activator.CreateInstance(modelType);
|
||||||
|
|
@ -101,6 +87,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
|
public virtual Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
|
||||||
{
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
if (request.ContentLength == 0)
|
if (request.ContentLength == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -117,50 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
|
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
|
||||||
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context);
|
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an <see cref="Encoding"/> based on <paramref name="context"/>'s
|
|
||||||
/// character set.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// An <see cref="Encoding"/> based on <paramref name="context"/>'s
|
|
||||||
/// character set. <c>null</c> if no supported encoding was found.
|
|
||||||
/// </returns>
|
|
||||||
protected Encoding SelectCharacterEncoding(InputFormatterContext context)
|
|
||||||
{
|
|
||||||
var request = context.HttpContext.Request;
|
|
||||||
|
|
||||||
if (request.ContentType != null)
|
|
||||||
{
|
|
||||||
var encoding = MediaType.GetEncoding(request.ContentType);
|
|
||||||
if (encoding != null)
|
|
||||||
{
|
|
||||||
foreach (var supportedEncoding in SupportedEncodings)
|
|
||||||
{
|
|
||||||
if (string.Equals(
|
|
||||||
encoding.WebName,
|
|
||||||
supportedEncoding.WebName,
|
|
||||||
StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return supportedEncoding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SupportedEncodings.Count > 0)
|
|
||||||
{
|
|
||||||
return SupportedEncodings[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// No supported encoding was found so there is no way for us to start reading.
|
|
||||||
context.ModelState.TryAddModelError(
|
|
||||||
context.ModelName,
|
|
||||||
Resources.FormatInputFormatterNoEncoding(GetType().FullName));
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
|
public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// 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 System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Core;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reads an object from a request body with a text format.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class TextInputFormatter : InputFormatter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns UTF8 Encoding without BOM and throws on invalid bytes.
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly Encoding UTF8EncodingWithoutBOM
|
||||||
|
= new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes.
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly Encoding UTF16EncodingLittleEndian
|
||||||
|
= new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the mutable collection of character encodings supported by
|
||||||
|
/// this <see cref="TextInputFormatter"/>. The encodings are
|
||||||
|
/// used when reading the data.
|
||||||
|
/// </summary>
|
||||||
|
public IList<Encoding> SupportedEncodings { get; } = new List<Encoding>();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
var selectedEncoding = SelectCharacterEncoding(context);
|
||||||
|
if (selectedEncoding == null)
|
||||||
|
{
|
||||||
|
var message = Resources.FormatUnsupportedContentType(
|
||||||
|
context.HttpContext.Request.ContentType);
|
||||||
|
|
||||||
|
var exception = new UnsupportedContentTypeException(message);
|
||||||
|
context.ModelState.AddModelError(context.ModelName, exception, context.Metadata);
|
||||||
|
|
||||||
|
return InputFormatterResult.FailureAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadRequestBodyAsync(context, selectedEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads an object from the request body.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
|
||||||
|
/// <param name="encoding">The <see cref="Encoding"/> used to read the request body.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> that on completion deserializes the request body.</returns>
|
||||||
|
public abstract Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an <see cref="Encoding"/> based on <paramref name="context"/>'s
|
||||||
|
/// character set.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="InputFormatterContext"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// An <see cref="Encoding"/> based on <paramref name="context"/>'s
|
||||||
|
/// character set. <c>null</c> if no supported encoding was found.
|
||||||
|
/// </returns>
|
||||||
|
protected Encoding SelectCharacterEncoding(InputFormatterContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SupportedEncodings?.Count == 0)
|
||||||
|
{
|
||||||
|
var message = Resources.FormatTextInputFormatter_SupportedEncodingsMustNotBeEmpty(nameof(SupportedEncodings));
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
|
||||||
|
var requestEncoding = request.ContentType == null ? null : MediaType.GetEncoding(request.ContentType);
|
||||||
|
if (requestEncoding != null)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < SupportedEncodings.Count; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(
|
||||||
|
requestEncoding.WebName,
|
||||||
|
SupportedEncodings[i].WebName,
|
||||||
|
StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return SupportedEncodings[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client specified an encoding in the content type header of the request
|
||||||
|
// but we don't understand it. In this situation we don't try to pick any other encoding
|
||||||
|
// from the list of supported encodings and read the body with that encoding.
|
||||||
|
// Instead, we return null and that will translate later on into a 415 Unsupported Media Type
|
||||||
|
// response.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to do our best effort to read the body of the request even in the
|
||||||
|
// cases where the client doesn't send a content type header or sends a content
|
||||||
|
// type header without encoding. For that reason we pick the first encoding of the
|
||||||
|
// list of supported encodings and try to use that to read the body. This encoding
|
||||||
|
// is UTF-8 by default on our formatters, which generally is a safe choice for the
|
||||||
|
// encoding.
|
||||||
|
return SupportedEncodings[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1098,6 +1098,22 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0);
|
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of '{0}' must not be empty. Add at least one supported encoding.
|
||||||
|
/// </summary>
|
||||||
|
internal static string TextInputFormatter_SupportedEncodingsMustNotBeEmpty
|
||||||
|
{
|
||||||
|
get { return GetString("TextInputFormatter_SupportedEncodingsMustNotBeEmpty"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of '{0}' must not be empty. Add at least one supported encoding.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatTextInputFormatter_SupportedEncodingsMustNotBeEmpty(object p0)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("TextInputFormatter_SupportedEncodingsMustNotBeEmpty"), p0);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<root>
|
<root>
|
||||||
<!--
|
<!--
|
||||||
Microsoft ResX Schema
|
Microsoft ResX Schema
|
||||||
|
|
||||||
Version 2.0
|
Version 2.0
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
The primary goals of this format is to allow a simple XML format
|
||||||
that is mostly human readable. The generation and parsing of the
|
that is mostly human readable. The generation and parsing of the
|
||||||
various data types are done through the TypeConverter classes
|
various data types are done through the TypeConverter classes
|
||||||
associated with the data types.
|
associated with the data types.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
... ado.net/XML headers & schema ...
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
<resheader name="version">2.0</resheader>
|
<resheader name="version">2.0</resheader>
|
||||||
|
|
@ -26,36 +26,36 @@
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
<comment>This is a comment</comment>
|
<comment>This is a comment</comment>
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
There are any number of "resheader" rows that contain simple
|
||||||
name/value pairs.
|
name/value pairs.
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
Each data row contains a name, and value. The row also contains a
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
text/value conversion through the TypeConverter architecture.
|
text/value conversion through the TypeConverter architecture.
|
||||||
Classes that don't support this are serialized and stored with the
|
Classes that don't support this are serialized and stored with the
|
||||||
mimetype set.
|
mimetype set.
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
The mimetype is used for serialized objects, and tells the
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
read any of the formats listed below.
|
read any of the formats listed below.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
value : The object must be serialized with
|
value : The object must be serialized with
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
value : The object must be serialized into a byte array
|
value : The object must be serialized into a byte array
|
||||||
: using a System.ComponentModel.TypeConverter
|
: using a System.ComponentModel.TypeConverter
|
||||||
: and then encoded with base64 encoding.
|
: and then encoded with base64 encoding.
|
||||||
-->
|
-->
|
||||||
|
|
@ -331,4 +331,7 @@
|
||||||
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
|
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
|
||||||
<value>The field {0} must be a number.</value>
|
<value>The field {0} must be a number.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TextInputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve">
|
||||||
|
<value>The list of '{0}' must not be empty. Add at least one supported encoding.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
|
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
@ -13,9 +14,9 @@ using Newtonsoft.Json;
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An <see cref="InputFormatter"/> for JSON content.
|
/// An <see cref="TextInputFormatter"/> for JSON content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class JsonInputFormatter : InputFormatter
|
public class JsonInputFormatter : TextInputFormatter
|
||||||
{
|
{
|
||||||
private readonly IArrayPool<char> _charPool;
|
private readonly IArrayPool<char> _charPool;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
@ -116,22 +117,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||||
|
InputFormatterContext context,
|
||||||
|
Encoding encoding)
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the character encoding for the content.
|
if (encoding == null)
|
||||||
var effectiveEncoding = SelectCharacterEncoding(context);
|
|
||||||
if (effectiveEncoding == null)
|
|
||||||
{
|
{
|
||||||
return InputFormatterResult.FailureAsync();
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
using (var streamReader = context.ReaderFactory(request.Body, effectiveEncoding))
|
using (var streamReader = context.ReaderFactory(request.Body, encoding))
|
||||||
{
|
{
|
||||||
using (var jsonReader = new JsonTextReader(streamReader))
|
using (var jsonReader = new JsonTextReader(streamReader))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.JsonPatch;
|
using Microsoft.AspNetCore.JsonPatch;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
|
using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal;
|
||||||
|
|
@ -38,14 +39,21 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public async override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||||
|
InputFormatterContext context,
|
||||||
|
Encoding encoding)
|
||||||
{
|
{
|
||||||
if (context == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await base.ReadRequestBodyAsync(context);
|
if (encoding == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await base.ReadRequestBodyAsync(context, encoding);
|
||||||
if (!result.HasError)
|
if (!result.HasError)
|
||||||
{
|
{
|
||||||
var jsonPatchDocument = (IJsonPatchDocument)result.Model;
|
var jsonPatchDocument = (IJsonPatchDocument)result.Model;
|
||||||
|
|
@ -61,6 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanRead(InputFormatterContext context)
|
public override bool CanRead(InputFormatterContext context)
|
||||||
{
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
var modelTypeInfo = context.ModelType.GetTypeInfo();
|
var modelTypeInfo = context.ModelType.GetTypeInfo();
|
||||||
if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) ||
|
if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) ||
|
||||||
!modelTypeInfo.IsGenericType)
|
!modelTypeInfo.IsGenericType)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ using System.Xml;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// This class handles deserialization of input XML data
|
/// This class handles deserialization of input XML data
|
||||||
/// to strongly-typed objects using <see cref="DataContractSerializer"/>.
|
/// to strongly-typed objects using <see cref="DataContractSerializer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class XmlDataContractSerializerInputFormatter : InputFormatter
|
public class XmlDataContractSerializerInputFormatter : TextInputFormatter
|
||||||
{
|
{
|
||||||
private DataContractSerializerSettings _serializerSettings;
|
private DataContractSerializerSettings _serializerSettings;
|
||||||
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||||
|
|
@ -86,16 +85,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
|
||||||
{
|
{
|
||||||
var effectiveEncoding = SelectCharacterEncoding(context);
|
if (context == null)
|
||||||
if (effectiveEncoding == null)
|
|
||||||
{
|
{
|
||||||
return InputFormatterResult.FailureAsync();
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding))
|
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
|
||||||
{
|
{
|
||||||
var type = GetSerializableType(context.ModelType);
|
var type = GetSerializableType(context.ModelType);
|
||||||
var serializer = GetCachedSerializer(type);
|
var serializer = GetCachedSerializer(type);
|
||||||
|
|
@ -197,6 +200,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// <returns>The <see cref="DataContractSerializer"/> instance.</returns>
|
/// <returns>The <see cref="DataContractSerializer"/> instance.</returns>
|
||||||
protected virtual DataContractSerializer GetCachedSerializer(Type type)
|
protected virtual DataContractSerializer GetCachedSerializer(Type type)
|
||||||
{
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
}
|
||||||
|
|
||||||
object serializer;
|
object serializer;
|
||||||
if (!_serializerCache.TryGetValue(type, out serializer))
|
if (!_serializerCache.TryGetValue(type, out serializer))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ using System.Xml.Serialization;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
using Microsoft.AspNetCore.Mvc.Formatters.Xml;
|
||||||
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Formatters
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
{
|
{
|
||||||
|
|
@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// This class handles deserialization of input XML data
|
/// This class handles deserialization of input XML data
|
||||||
/// to strongly-typed objects using <see cref="XmlSerializer"/>
|
/// to strongly-typed objects using <see cref="XmlSerializer"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class XmlSerializerInputFormatter : InputFormatter
|
public class XmlSerializerInputFormatter : TextInputFormatter
|
||||||
{
|
{
|
||||||
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
private ConcurrentDictionary<Type, object> _serializerCache = new ConcurrentDictionary<Type, object>();
|
||||||
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
|
private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas();
|
||||||
|
|
@ -65,16 +64,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||||
|
InputFormatterContext context,
|
||||||
|
Encoding encoding)
|
||||||
{
|
{
|
||||||
var effectiveEncoding = SelectCharacterEncoding(context);
|
if (context == null)
|
||||||
if (effectiveEncoding == null)
|
|
||||||
{
|
{
|
||||||
return InputFormatterResult.FailureAsync();
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), effectiveEncoding))
|
using (var xmlReader = CreateXmlReader(new NonDisposableStream(request.Body), encoding))
|
||||||
{
|
{
|
||||||
var type = GetSerializableType(context.ModelType);
|
var type = GetSerializableType(context.ModelType);
|
||||||
|
|
||||||
|
|
@ -171,6 +176,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
/// <returns>The <see cref="XmlSerializer"/> instance.</returns>
|
/// <returns>The <see cref="XmlSerializer"/> instance.</returns>
|
||||||
protected virtual XmlSerializer GetCachedSerializer(Type type)
|
protected virtual XmlSerializer GetCachedSerializer(Type type)
|
||||||
{
|
{
|
||||||
|
if (type == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(type));
|
||||||
|
}
|
||||||
|
|
||||||
object serializer;
|
object serializer;
|
||||||
if (!_serializerCache.TryGetValue(type, out serializer))
|
if (!_serializerCache.TryGetValue(type, out serializer))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||||
|
|
@ -1455,11 +1456,13 @@ namespace Microsoft.AspNetCore.Mvc.Description
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MockInputFormatter : InputFormatter
|
private class MockInputFormatter : TextInputFormatter
|
||||||
{
|
{
|
||||||
public List<Type> SupportedTypes { get; } = new List<Type>();
|
public List<Type> SupportedTypes { get; } = new List<Type>();
|
||||||
|
|
||||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||||
|
InputFormatterContext context,
|
||||||
|
Encoding effectiveEncoding)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http.Internal;
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
// 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 System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http.Internal;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Formatters
|
||||||
|
{
|
||||||
|
public class TextInputFormatterTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadAsync_ReturnsFailure_IfItCanNotUnderstandTheContentTypeEncoding()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = new TestFormatter();
|
||||||
|
formatter.SupportedEncodings.Add(Encoding.ASCII);
|
||||||
|
|
||||||
|
var context = new InputFormatterContext(
|
||||||
|
new DefaultHttpContext(),
|
||||||
|
"something",
|
||||||
|
new ModelStateDictionary(),
|
||||||
|
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||||
|
(stream, encoding) => new StreamReader(stream, encoding));
|
||||||
|
|
||||||
|
context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
|
||||||
|
context.HttpContext.Request.ContentLength = 1;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await formatter.ReadAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(true, result.HasError);
|
||||||
|
Assert.Equal(true, context.ModelState.ContainsKey("something"));
|
||||||
|
Assert.Equal(1, context.ModelState["something"].Errors.Count);
|
||||||
|
|
||||||
|
var error = context.ModelState["something"].Errors[0];
|
||||||
|
Assert.IsType<UnsupportedContentTypeException>(error.Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectCharacterEncoding_ThrowsInvalidOperationException_IfItDoesNotHaveAValidEncoding()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = new TestFormatter();
|
||||||
|
|
||||||
|
var context = new InputFormatterContext(
|
||||||
|
new DefaultHttpContext(),
|
||||||
|
"something",
|
||||||
|
new ModelStateDictionary(),
|
||||||
|
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||||
|
(stream, encoding) => new StreamReader(stream, encoding));
|
||||||
|
|
||||||
|
context.HttpContext.Request.ContentLength = 1;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<InvalidOperationException>(() => formatter.TestSelectCharacterEncoding(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectCharacterEncoding_ReturnsNull_IfItCanNotUnderstandContentTypeEncoding()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = new TestFormatter();
|
||||||
|
formatter.SupportedEncodings.Add(Encoding.UTF32);
|
||||||
|
|
||||||
|
var context = new InputFormatterContext(
|
||||||
|
new DefaultHttpContext(),
|
||||||
|
"something",
|
||||||
|
new ModelStateDictionary(),
|
||||||
|
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||||
|
(stream, encoding) => new StreamReader(stream, encoding));
|
||||||
|
|
||||||
|
context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = formatter.TestSelectCharacterEncoding(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectCharacterEncoding_ReturnsContentTypeEncoding_IfItCanUnderstandIt()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = new TestFormatter();
|
||||||
|
formatter.SupportedEncodings.Add(Encoding.UTF32);
|
||||||
|
formatter.SupportedEncodings.Add(Encoding.UTF8);
|
||||||
|
|
||||||
|
var context = new InputFormatterContext(
|
||||||
|
new DefaultHttpContext(),
|
||||||
|
"something",
|
||||||
|
new ModelStateDictionary(),
|
||||||
|
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||||
|
(stream, encoding) => new StreamReader(stream, encoding));
|
||||||
|
|
||||||
|
context.HttpContext.Request.ContentType = "application/json;charset=utf-8";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = formatter.TestSelectCharacterEncoding(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Encoding.UTF8, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("application/json")]
|
||||||
|
[InlineData("")]
|
||||||
|
public void SelectCharacterEncoding_ReturnsFirstEncoding_IfContentTypeIsNotSpecifiedOrDoesNotHaveEncoding(string contentType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var formatter = new TestFormatter();
|
||||||
|
formatter.SupportedEncodings.Add(Encoding.UTF8);
|
||||||
|
formatter.SupportedEncodings.Add(Encoding.UTF32);
|
||||||
|
|
||||||
|
var context = new InputFormatterContext(
|
||||||
|
new DefaultHttpContext(),
|
||||||
|
"something",
|
||||||
|
new ModelStateDictionary(),
|
||||||
|
new EmptyModelMetadataProvider().GetMetadataForType(typeof(object)),
|
||||||
|
(stream, encoding) => new StreamReader(stream, encoding));
|
||||||
|
|
||||||
|
context.HttpContext.Request.ContentType = contentType;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = formatter.TestSelectCharacterEncoding(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Encoding.UTF8, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestFormatter : TextInputFormatter
|
||||||
|
{
|
||||||
|
private readonly object _object;
|
||||||
|
|
||||||
|
public TestFormatter() : this(null) { }
|
||||||
|
|
||||||
|
public TestFormatter(object @object)
|
||||||
|
{
|
||||||
|
_object = @object;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<Type> SupportedTypes { get; } = new List<Type>();
|
||||||
|
|
||||||
|
protected override bool CanReadType(Type type)
|
||||||
|
{
|
||||||
|
return SupportedTypes.Count == 0 ? true : SupportedTypes.Contains(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
|
||||||
|
{
|
||||||
|
return InputFormatterResult.SuccessAsync(_object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Encoding TestSelectCharacterEncoding(InputFormatterContext context)
|
||||||
|
{
|
||||||
|
return SelectCharacterEncoding(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -312,7 +312,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class XyzFormatter : InputFormatter
|
private class XyzFormatter : TextInputFormatter
|
||||||
{
|
{
|
||||||
public XyzFormatter()
|
public XyzFormatter()
|
||||||
{
|
{
|
||||||
|
|
@ -325,7 +325,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(
|
||||||
|
InputFormatterContext context,
|
||||||
|
Encoding effectiveEncoding)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Your input is bad!");
|
throw new InvalidOperationException("Your input is bad!");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace FormatterWebSite
|
namespace FormatterWebSite
|
||||||
{
|
{
|
||||||
public class StringInputFormatter : InputFormatter
|
public class StringInputFormatter : TextInputFormatter
|
||||||
{
|
{
|
||||||
public StringInputFormatter()
|
public StringInputFormatter()
|
||||||
{
|
{
|
||||||
|
|
@ -20,14 +20,8 @@ namespace FormatterWebSite
|
||||||
SupportedEncodings.Add(Encoding.Unicode);
|
SupportedEncodings.Add(Encoding.Unicode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
|
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
|
||||||
{
|
{
|
||||||
var effectiveEncoding = SelectCharacterEncoding(context);
|
|
||||||
if (effectiveEncoding == null)
|
|
||||||
{
|
|
||||||
return InputFormatterResult.FailureAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = context.HttpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
using (var reader = new StreamReader(request.Body, effectiveEncoding))
|
using (var reader = new StreamReader(request.Body, effectiveEncoding))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue