Skip to content

Commit 783adbf

Browse files
Fix EmptyBodyBehavior with empty content-type (#38092)
1 parent 337b53c commit 783adbf

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

src/Mvc/Mvc.Core/src/ModelBinding/Binders/BodyModelBinder.cs

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.IO;
99
using System.Text;
1010
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Http.Features;
1112
using Microsoft.AspNetCore.Mvc.Core;
1213
using Microsoft.AspNetCore.Mvc.Formatters;
1314
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -143,6 +144,17 @@ public async Task BindModelAsync(ModelBindingContext bindingContext)
143144

144145
if (formatter == null)
145146
{
147+
if (AllowEmptyBody)
148+
{
149+
var hasBody = httpContext.Features.Get<IHttpRequestBodyDetectionFeature>()?.CanHaveBody;
150+
hasBody ??= httpContext.Request.ContentLength is not null && httpContext.Request.ContentLength == 0;
151+
if (hasBody == false)
152+
{
153+
bindingContext.Result = ModelBindingResult.Success(model: null);
154+
return;
155+
}
156+
}
157+
146158
_logger.NoInputFormatterSelected(formatterContext);
147159

148160
var message = Resources.FormatUnsupportedContentType(httpContext.Request.ContentType);

src/Mvc/Mvc.Core/test/ModelBinding/Binders/BodyModelBinderTests.cs

+56
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,62 @@ public async Task BindModel_PassesAllowEmptyInputOptionViaContext(bool treatEmpt
195195
Times.Once);
196196
}
197197

198+
[Fact]
199+
public async Task BindModel_SetsModelIfAllowEmpty()
200+
{
201+
// Arrange
202+
var mockInputFormatter = new Mock<IInputFormatter>();
203+
mockInputFormatter.Setup(f => f.CanRead(It.IsAny<InputFormatterContext>()))
204+
.Returns(false);
205+
var inputFormatter = mockInputFormatter.Object;
206+
207+
var provider = new TestModelMetadataProvider();
208+
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
209+
210+
var bindingContext = GetBindingContext(
211+
typeof(Person),
212+
metadataProvider: provider);
213+
bindingContext.BinderModelName = "custom";
214+
215+
var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption : true);
216+
217+
// Act
218+
await binder.BindModelAsync(bindingContext);
219+
220+
// Assert
221+
Assert.True(bindingContext.Result.IsModelSet);
222+
Assert.Null(bindingContext.Result.Model);
223+
Assert.True(bindingContext.ModelState.IsValid);
224+
}
225+
226+
[Fact]
227+
public async Task BindModel_FailsIfNotAllowEmpty()
228+
{
229+
// Arrange
230+
var mockInputFormatter = new Mock<IInputFormatter>();
231+
mockInputFormatter.Setup(f => f.CanRead(It.IsAny<InputFormatterContext>()))
232+
.Returns(false);
233+
var inputFormatter = mockInputFormatter.Object;
234+
235+
var provider = new TestModelMetadataProvider();
236+
provider.ForType<Person>().BindingDetails(d => d.BindingSource = BindingSource.Body);
237+
238+
var bindingContext = GetBindingContext(
239+
typeof(Person),
240+
metadataProvider: provider);
241+
bindingContext.BinderModelName = "custom";
242+
243+
var binder = CreateBinder(new[] { inputFormatter }, treatEmptyInputAsDefaultValueOption: false);
244+
245+
// Act
246+
await binder.BindModelAsync(bindingContext);
247+
248+
// Assert
249+
Assert.False(bindingContext.ModelState.IsValid);
250+
Assert.Single(bindingContext.ModelState[bindingContext.BinderModelName].Errors);
251+
Assert.Equal("Unsupported content type ''.", bindingContext.ModelState[bindingContext.BinderModelName].Errors[0].Exception.Message);
252+
}
253+
198254
// Throwing InputFormatterException
199255
[Fact]
200256
public async Task BindModel_CustomFormatter_ThrowingInputFormatterException_AddsErrorToModelState()

src/Mvc/test/Mvc.FunctionalTests/InputFormatterTests.cs

+29
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,20 @@ public async Task BodyIsRequiredByDefault()
188188
});
189189
}
190190

191+
[Fact]
192+
public async Task BodyIsRequiredByDefaultFailsWithEmptyBody()
193+
{
194+
var content = new ByteArrayContent(Array.Empty<byte>());
195+
Assert.Null(content.Headers.ContentType);
196+
Assert.Equal(0, content.Headers.ContentLength);
197+
198+
// Act
199+
var response = await Client.PostAsync($"Home/{nameof(HomeController.DefaultBody)}", content);
200+
201+
// Assert
202+
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
203+
}
204+
191205
[Fact]
192206
public async Task OptionalFromBodyWorks()
193207
{
@@ -197,4 +211,19 @@ public async Task OptionalFromBodyWorks()
197211
// Assert
198212
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
199213
}
214+
215+
[Fact]
216+
public async Task OptionalFromBodyWorksWithEmptyRequest()
217+
{
218+
// Arrange
219+
var content = new ByteArrayContent(Array.Empty<byte>());
220+
Assert.Null(content.Headers.ContentType);
221+
Assert.Equal(0, content.Headers.ContentLength);
222+
223+
// Act
224+
var response = await Client.PostAsync($"Home/{nameof(HomeController.OptionalBody)}", content);
225+
226+
// Assert
227+
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
228+
}
200229
}

0 commit comments

Comments
 (0)