From 69e913438a0fa827395fe73b085a07cbc57e66ff Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 4 Nov 2021 13:35:15 -0700 Subject: [PATCH 1/3] Fix EmptyBodyBehavior with empty content-type --- .../ModelBinding/Binders/BodyModelBinder.cs | 5 ++++ .../InputFormatterTests.cs | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs index 2bfe03d14b97..b192d9cf84bd 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -143,6 +143,11 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) if (formatter == null) { + if (AllowEmptyBody && httpContext.Request.ContentLength == 0) + { + return; + } + _logger.NoInputFormatterSelected(formatterContext); var message = Resources.FormatUnsupportedContentType(httpContext.Request.ContentType); diff --git a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs index 984cf3d98185..2911d85a82ed 100644 --- a/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs +++ b/src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs @@ -188,6 +188,20 @@ public async Task BodyIsRequiredByDefault() }); } + [Fact] + public async Task BodyIsRequiredByDefaultFailsWithEmptyBody() + { + var content = new ByteArrayContent(Array.Empty()); + Assert.Null(content.Headers.ContentType); + Assert.Equal(0, content.Headers.ContentLength); + + // Act + var response = await Client.PostAsync($"Home/{nameof(HomeController.DefaultBody)}", content); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); + } + [Fact] public async Task OptionalFromBodyWorks() { @@ -197,4 +211,19 @@ public async Task OptionalFromBodyWorks() // Assert await response.AssertStatusCodeAsync(HttpStatusCode.OK); } + + [Fact] + public async Task OptionalFromBodyWorksWithEmptyRequest() + { + // Arrange + var content = new ByteArrayContent(Array.Empty()); + Assert.Null(content.Headers.ContentType); + Assert.Equal(0, content.Headers.ContentLength); + + // Act + var response = await Client.PostAsync($"Home/{nameof(HomeController.OptionalBody)}", content); + + // Assert + await response.AssertStatusCodeAsync(HttpStatusCode.OK); + } } From 002cbd0f2971e6adc7724371d7ac95701200beaf Mon Sep 17 00:00:00 2001 From: Brennan Date: Thu, 4 Nov 2021 20:21:14 -0700 Subject: [PATCH 2/3] fb --- src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs index b192d9cf84bd..612f26a1de07 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -8,6 +8,7 @@ using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -143,8 +144,9 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) if (formatter == null) { - if (AllowEmptyBody && httpContext.Request.ContentLength == 0) + if (AllowEmptyBody && httpContext.Features.Get()?.CanHaveBody == false) { + bindingContext.Result = ModelBindingResult.Success(model: null); return; } From ff7b27af94d10b301d3cae4e058c9ba956f4dbae Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 12 Nov 2021 11:23:50 -0800 Subject: [PATCH 3/3] fb --- .../ModelBinding/Binders/BodyModelBinder.cs | 11 +++- .../Binders/BodyModelBinderTests.cs | 56 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs index 612f26a1de07..4efbc86c5f2f 100644 --- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs +++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs @@ -144,10 +144,15 @@ public async Task BindModelAsync(ModelBindingContext bindingContext) if (formatter == null) { - if (AllowEmptyBody && httpContext.Features.Get()?.CanHaveBody == false) + if (AllowEmptyBody) { - bindingContext.Result = ModelBindingResult.Success(model: null); - return; + var hasBody = httpContext.Features.Get()?.CanHaveBody; + hasBody ??= httpContext.Request.ContentLength is not null && httpContext.Request.ContentLength == 0; + if (hasBody == false) + { + bindingContext.Result = ModelBindingResult.Success(model: null); + return; + } } _logger.NoInputFormatterSelected(formatterContext); diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs index 76ceb024c10f..a63505a7ccb7 100644 --- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs +++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs @@ -195,6 +195,62 @@ public async Task BindModel_PassesAllowEmptyInputOptionViaContext(bool treatEmpt Times.Once); } + [Fact] + public async Task BindModel_SetsModelIfAllowEmpty() + { + // Arrange + var mockInputFormatter = new Mock(); + mockInputFormatter.Setup(f => f.CanRead(It.IsAny())) + .Returns(false); + var inputFormatter = mockInputFormatter.Object; + + var provider = new TestModelMetadataProvider(); + provider.ForType().BindingDetails(d => d.BindingSource = BindingSource.Body); + + var bindingContext = GetBindingContext( + typeof(Person), + metadataProvider: provider); + bindingContext.BinderModelName = "custom"; + + var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption : true); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.True(bindingContext.Result.IsModelSet); + Assert.Null(bindingContext.Result.Model); + Assert.True(bindingContext.ModelState.IsValid); + } + + [Fact] + public async Task BindModel_FailsIfNotAllowEmpty() + { + // Arrange + var mockInputFormatter = new Mock(); + mockInputFormatter.Setup(f => f.CanRead(It.IsAny())) + .Returns(false); + var inputFormatter = mockInputFormatter.Object; + + var provider = new TestModelMetadataProvider(); + provider.ForType().BindingDetails(d => d.BindingSource = BindingSource.Body); + + var bindingContext = GetBindingContext( + typeof(Person), + metadataProvider: provider); + bindingContext.BinderModelName = "custom"; + + var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption: false); + + // Act + await binder.BindModelAsync(bindingContext); + + // Assert + Assert.False(bindingContext.ModelState.IsValid); + Assert.Single(bindingContext.ModelState[bindingContext.BinderModelName].Errors); + Assert.Equal("Unsupported content type ''.", bindingContext.ModelState[bindingContext.BinderModelName].Errors[0].Exception.Message); + } + // Throwing InputFormatterException [Fact] public async Task BindModel_CustomFormatter_ThrowingInputFormatterException_AddsErrorToModelState()