From fe639f028fb3aac96da779a2484e4d9f49990df8 Mon Sep 17 00:00:00 2001 From: jacalvar Date: Mon, 1 Feb 2016 15:58:18 -0800 Subject: [PATCH] [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. --- .../Formatters/InputFormatter.cs | 85 ++------- .../Formatters/TextInputFormatter.cs | 124 +++++++++++++ .../Properties/Resources.Designer.cs | 16 ++ .../Resources.resx | 57 +++--- .../JsonInputFormatter.cs | 17 +- .../JsonPatchInputFormatter.cs | 17 +- ...XmlDataContractSerializerInputFormatter.cs | 22 ++- .../XmlSerializerInputFormatter.cs | 24 ++- .../DefaultApiDescriptionProviderTest.cs | 7 +- .../Formatters/InputFormatterTest.cs | 1 - .../Formatters/TextInputFormatterTest.cs | 167 ++++++++++++++++++ .../ModelBinding/BodyModelBinderTests.cs | 6 +- .../FormatterWebSite/StringInputFormatter.cs | 10 +- 13 files changed, 420 insertions(+), 133 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Formatters/TextInputFormatter.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/TextInputFormatterTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/InputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/InputFormatter.cs index f743faec66..f231f3f1eb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/InputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/InputFormatter.cs @@ -3,14 +3,9 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Core; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Formatters { @@ -19,33 +14,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public abstract class InputFormatter : IInputFormatter, IApiRequestFormatMetadataProvider { - /// - /// Returns UTF8 Encoding without BOM and throws on invalid bytes. - /// - protected static readonly Encoding UTF8EncodingWithoutBOM - = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - - /// - /// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes. - /// - protected static readonly Encoding UTF16EncodingLittleEndian - = new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true); - - /// - /// Gets the mutable collection of character encodings supported by - /// this . The encodings are - /// used when reading the data. - /// - public IList SupportedEncodings { get; } = new List(); - /// /// Gets the mutable collection of media type elements supported by /// this . /// public MediaTypeCollection SupportedMediaTypes { get; } = new MediaTypeCollection(); - protected object GetDefaultValueForType(Type modelType) + /// + /// Gets the default value for a given type. Used to return a default value when the body contains no content. + /// + /// The type of the value. + /// The default value for the type. + protected virtual object GetDefaultValueForType(Type modelType) { + if (modelType == null) + { + throw new ArgumentNullException(nameof(modelType)); + } + if (modelType.GetTypeInfo().IsValueType) { return Activator.CreateInstance(modelType); @@ -101,6 +87,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public virtual Task ReadAsync(InputFormatterContext context) { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + var request = context.HttpContext.Request; if (request.ContentLength == 0) { @@ -117,50 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// A that on completion deserializes the request body. public abstract Task ReadRequestBodyAsync(InputFormatterContext context); - /// - /// Returns an based on 's - /// character set. - /// - /// The . - /// - /// An based on 's - /// character set. null if no supported encoding was found. - /// - 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; - } - /// public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Formatters/TextInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/TextInputFormatter.cs new file mode 100644 index 0000000000..8967511c00 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Formatters/TextInputFormatter.cs @@ -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 +{ + /// + /// Reads an object from a request body with a text format. + /// + public abstract class TextInputFormatter : InputFormatter + { + /// + /// Returns UTF8 Encoding without BOM and throws on invalid bytes. + /// + protected static readonly Encoding UTF8EncodingWithoutBOM + = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + /// + /// Returns UTF16 Encoding which uses littleEndian byte order with BOM and throws on invalid bytes. + /// + protected static readonly Encoding UTF16EncodingLittleEndian + = new UnicodeEncoding(bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true); + + /// + /// Gets the mutable collection of character encodings supported by + /// this . The encodings are + /// used when reading the data. + /// + public IList SupportedEncodings { get; } = new List(); + + /// + public override Task 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); + } + + /// + /// Reads an object from the request body. + /// + /// The . + /// The used to read the request body. + /// A that on completion deserializes the request body. + public abstract Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding); + + /// + /// Returns an based on 's + /// character set. + /// + /// The . + /// + /// An based on 's + /// character set. null if no supported encoding was found. + /// + 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]; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 65d7e0b324..74eff64d28 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1098,6 +1098,22 @@ namespace Microsoft.AspNetCore.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0); } + /// + /// The list of '{0}' must not be empty. Add at least one supported encoding. + /// + internal static string TextInputFormatter_SupportedEncodingsMustNotBeEmpty + { + get { return GetString("TextInputFormatter_SupportedEncodingsMustNotBeEmpty"); } + } + + /// + /// The list of '{0}' must not be empty. Add at least one supported encoding. + /// + 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) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index 9bb383b191..da40c89bc1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -1,17 +1,17 @@  - @@ -331,4 +331,7 @@ The field {0} must be a number. + + The list of '{0}' must not be empty. Add at least one supported encoding. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs index 98ab34056c..45cb73608a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonInputFormatter.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -13,9 +14,9 @@ using Newtonsoft.Json; namespace Microsoft.AspNetCore.Mvc.Formatters { /// - /// An for JSON content. + /// An for JSON content. /// - public class JsonInputFormatter : InputFormatter + public class JsonInputFormatter : TextInputFormatter { private readonly IArrayPool _charPool; private readonly ILogger _logger; @@ -116,22 +117,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } /// - public override Task ReadRequestBodyAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync( + InputFormatterContext context, + Encoding encoding) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - // Get the character encoding for the content. - var effectiveEncoding = SelectCharacterEncoding(context); - if (effectiveEncoding == null) + if (encoding == null) { - return InputFormatterResult.FailureAsync(); + throw new ArgumentNullException(nameof(encoding)); } 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)) { diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs index ab6c79ea7f..dde4b56291 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonPatchInputFormatter.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.JsonPatch; using Microsoft.AspNetCore.Mvc.Formatters.Json.Internal; @@ -38,14 +39,21 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } /// - public async override Task ReadRequestBodyAsync(InputFormatterContext context) + public async override Task ReadRequestBodyAsync( + InputFormatterContext context, + Encoding encoding) { if (context == null) { 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) { var jsonPatchDocument = (IJsonPatchDocument)result.Model; @@ -61,6 +69,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// public override bool CanRead(InputFormatterContext context) { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + var modelTypeInfo = context.ModelType.GetTypeInfo(); if (!typeof(IJsonPatchDocument).GetTypeInfo().IsAssignableFrom(modelTypeInfo) || !modelTypeInfo.IsGenericType) diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs index 50a8cb7f45..73e6ba165b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlDataContractSerializerInputFormatter.cs @@ -12,7 +12,6 @@ using System.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Formatters { @@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// This class handles deserialization of input XML data /// to strongly-typed objects using . /// - public class XmlDataContractSerializerInputFormatter : InputFormatter + public class XmlDataContractSerializerInputFormatter : TextInputFormatter { private DataContractSerializerSettings _serializerSettings; private ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); @@ -86,16 +85,20 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } /// - public override Task ReadRequestBodyAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { - var effectiveEncoding = SelectCharacterEncoding(context); - if (effectiveEncoding == null) + if (context == null) { - return InputFormatterResult.FailureAsync(); + throw new ArgumentNullException(nameof(context)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); } 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 serializer = GetCachedSerializer(type); @@ -197,6 +200,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The instance. protected virtual DataContractSerializer GetCachedSerializer(Type type) { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + object serializer; if (!_serializerCache.TryGetValue(type, out serializer)) { diff --git a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs index a8c65c16b0..92e1eee58b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Formatters.Xml/XmlSerializerInputFormatter.cs @@ -12,7 +12,6 @@ using System.Xml.Serialization; using Microsoft.AspNetCore.Mvc.Formatters.Xml; using Microsoft.AspNetCore.Mvc.Formatters.Xml.Internal; using Microsoft.AspNetCore.Mvc.Internal; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Mvc.Formatters { @@ -20,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// This class handles deserialization of input XML data /// to strongly-typed objects using /// - public class XmlSerializerInputFormatter : InputFormatter + public class XmlSerializerInputFormatter : TextInputFormatter { private ConcurrentDictionary _serializerCache = new ConcurrentDictionary(); private readonly XmlDictionaryReaderQuotas _readerQuotas = FormattingUtilities.GetDefaultXmlReaderQuotas(); @@ -65,16 +64,22 @@ namespace Microsoft.AspNetCore.Mvc.Formatters } /// - public override Task ReadRequestBodyAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync( + InputFormatterContext context, + Encoding encoding) { - var effectiveEncoding = SelectCharacterEncoding(context); - if (effectiveEncoding == null) + if (context == null) { - return InputFormatterResult.FailureAsync(); + throw new ArgumentNullException(nameof(context)); + } + + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); } 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); @@ -171,6 +176,11 @@ namespace Microsoft.AspNetCore.Mvc.Formatters /// The instance. protected virtual XmlSerializer GetCachedSerializer(Type type) { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + object serializer; if (!_serializerCache.TryGetValue(type, out serializer)) { diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs index b6ad84694f..eeaeabe56d 100644 --- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ActionConstraints; @@ -1455,11 +1456,13 @@ namespace Microsoft.AspNetCore.Mvc.Description public int Id { get; set; } } - private class MockInputFormatter : InputFormatter + private class MockInputFormatter : TextInputFormatter { public List SupportedTypes { get; } = new List(); - public override Task ReadRequestBodyAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync( + InputFormatterContext context, + Encoding effectiveEncoding) { throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/InputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/InputFormatterTest.cs index 09c210017f..6ddb2d3838 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/InputFormatterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/InputFormatterTest.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Net.Http.Headers; using Xunit; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/TextInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/TextInputFormatterTest.cs new file mode 100644 index 0000000000..54381d6994 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/TextInputFormatterTest.cs @@ -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(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(() => 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 SupportedTypes { get; } = new List(); + + protected override bool CanReadType(Type type) + { + return SupportedTypes.Count == 0 ? true : SupportedTypes.Contains(type); + } + + public override Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) + { + return InputFormatterResult.SuccessAsync(_object); + } + + public Encoding TestSelectCharacterEncoding(InputFormatterContext context) + { + return SelectCharacterEncoding(context); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs index b49a518293..1b72b07940 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/BodyModelBinderTests.cs @@ -312,7 +312,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding public string Name { get; set; } } - private class XyzFormatter : InputFormatter + private class XyzFormatter : TextInputFormatter { public XyzFormatter() { @@ -325,7 +325,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding return true; } - public override Task ReadRequestBodyAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync( + InputFormatterContext context, + Encoding effectiveEncoding) { throw new InvalidOperationException("Your input is bad!"); } diff --git a/test/WebSites/FormatterWebSite/StringInputFormatter.cs b/test/WebSites/FormatterWebSite/StringInputFormatter.cs index c93abffdca..dc80e3dc62 100644 --- a/test/WebSites/FormatterWebSite/StringInputFormatter.cs +++ b/test/WebSites/FormatterWebSite/StringInputFormatter.cs @@ -10,7 +10,7 @@ using Microsoft.Net.Http.Headers; namespace FormatterWebSite { - public class StringInputFormatter : InputFormatter + public class StringInputFormatter : TextInputFormatter { public StringInputFormatter() { @@ -20,14 +20,8 @@ namespace FormatterWebSite SupportedEncodings.Add(Encoding.Unicode); } - public override Task ReadRequestBodyAsync(InputFormatterContext context) + public override Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) { - var effectiveEncoding = SelectCharacterEncoding(context); - if (effectiveEncoding == null) - { - return InputFormatterResult.FailureAsync(); - } - var request = context.HttpContext.Request; using (var reader = new StreamReader(request.Body, effectiveEncoding)) {