diff --git a/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenStore.cs b/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenStore.cs
index b35358fea2..513f92b1c6 100644
--- a/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenStore.cs
+++ b/src/Antiforgery/src/Internal/DefaultAntiforgeryTokenStore.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics;
+using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
@@ -57,7 +58,24 @@ namespace Microsoft.AspNetCore.Antiforgery
{
// Check the content-type before accessing the form collection to make sure
// we report errors gracefully.
- var form = await httpContext.Request.ReadFormAsync();
+ IFormCollection form;
+ try
+ {
+ form = await httpContext.Request.ReadFormAsync();
+ }
+ catch (InvalidDataException ex)
+ {
+ // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+ // Wrap it in an AntiforgeryValidationException and allow the caller to handle it as just another antiforgery failure.
+ throw new AntiforgeryValidationException(Resources.AntiforgeryToken_UnableToReadRequest, ex);
+ }
+ catch (IOException ex)
+ {
+ // Reading the request body (which happens as part of ReadFromAsync) may throw an exception if a client disconnects.
+ // Wrap it in an AntiforgeryValidationException and allow the caller to handle it as just another antiforgery failure.
+ throw new AntiforgeryValidationException(Resources.AntiforgeryToken_UnableToReadRequest, ex);
+ }
+
requestToken = form[_options.FormFieldName];
}
diff --git a/src/Antiforgery/src/Resources.resx b/src/Antiforgery/src/Resources.resx
index eeda70bc63..1bf0528d9e 100644
--- a/src/Antiforgery/src/Resources.resx
+++ b/src/Antiforgery/src/Resources.resx
@@ -136,6 +136,9 @@
Validation of the provided antiforgery token failed. The cookie token and the request token were swapped.
+
+ Unable to read the antiforgery request token from the posted form.
+
The provided antiforgery token was meant for user "{0}", but the current user is "{1}".
diff --git a/src/Antiforgery/test/DefaultAntiforgeryTokenStoreTest.cs b/src/Antiforgery/test/DefaultAntiforgeryTokenStoreTest.cs
index e4af2032f5..8456ee318a 100644
--- a/src/Antiforgery/test/DefaultAntiforgeryTokenStoreTest.cs
+++ b/src/Antiforgery/test/DefaultAntiforgeryTokenStoreTest.cs
@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -235,6 +237,56 @@ namespace Microsoft.AspNetCore.Antiforgery.Internal
Assert.Null(tokenSet.RequestToken);
}
+ [Fact]
+ public async Task GetRequestTokens_ReadFormAsyncThrowsIOException_ThrowsAntiforgeryValidationException()
+ {
+ // Arrange
+ var ioException = new IOException();
+ var httpContext = new Mock();
+
+ httpContext.Setup(r => r.Request.Cookies).Returns(Mock.Of());
+ httpContext.SetupGet(r => r.Request.HasFormContentType).Returns(true);
+ httpContext.Setup(r => r.Request.ReadFormAsync(It.IsAny())).Throws(ioException);
+
+ var options = new AntiforgeryOptions
+ {
+ Cookie = { Name = "cookie-name" },
+ FormFieldName = "form-field-name",
+ HeaderName = null,
+ };
+
+ var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options));
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => tokenStore.GetRequestTokensAsync(httpContext.Object));
+ Assert.Same(ioException, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetRequestTokens_ReadFormAsyncThrowsInvalidDataException_ThrowsAntiforgeryValidationException()
+ {
+ // Arrange
+ var exception = new InvalidDataException();
+ var httpContext = new Mock();
+
+ httpContext.Setup(r => r.Request.Cookies).Returns(Mock.Of());
+ httpContext.SetupGet(r => r.Request.HasFormContentType).Returns(true);
+ httpContext.Setup(r => r.Request.ReadFormAsync(It.IsAny())).Throws(exception);
+
+ var options = new AntiforgeryOptions
+ {
+ Cookie = { Name = "cookie-name" },
+ FormFieldName = "form-field-name",
+ HeaderName = null,
+ };
+
+ var tokenStore = new DefaultAntiforgeryTokenStore(new TestOptionsManager(options));
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => tokenStore.GetRequestTokensAsync(httpContext.Object));
+ Assert.Same(exception, ex.InnerException);
+ }
+
[Theory]
[InlineData(false, CookieSecurePolicy.SameAsRequest, null)]
[InlineData(true, CookieSecurePolicy.SameAsRequest, true)]
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
index df90de5039..a1c46eeba3 100644
--- a/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormFileValueProviderFactory.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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;
@@ -42,6 +42,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
catch (InvalidDataException ex)
{
+ // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+ // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+ throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+ }
+ catch (IOException ex)
+ {
+ // ReadFormAsync can throw IOException if the client disconnects.
+ // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
}
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs
index e7cbed8e11..d5a3686073 100644
--- a/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/FormValueProviderFactory.cs
@@ -44,6 +44,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
catch (InvalidDataException ex)
{
+ // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+ // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+ throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+ }
+ catch (IOException ex)
+ {
+ // ReadFormAsync can throw IOException if the client disconnects.
+ // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
}
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs b/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs
index fdd8eb2d93..d2fa89064a 100644
--- a/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/JQueryFormValueProviderFactory.cs
@@ -3,7 +3,10 @@
using System;
using System.Globalization;
+using System.IO;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
@@ -34,7 +37,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
var request = context.ActionContext.HttpContext.Request;
- var formCollection = await request.ReadFormAsync();
+ IFormCollection formCollection;
+ try
+ {
+ formCollection = await request.ReadFormAsync();
+ }
+ catch (InvalidDataException ex)
+ {
+ // ReadFormAsync can throw InvalidDataException if the form content is malformed.
+ // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+ throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+ }
+ catch (IOException ex)
+ {
+ // ReadFormAsync can throw IOException if the client disconnects.
+ // Wrap it in a ValueProviderException that the CompositeValueProvider special cases.
+ throw new ValueProviderException(Resources.FormatFailedToReadRequestForm(ex.Message), ex);
+ }
var valueProvider = new JQueryFormValueProvider(
BindingSource.Form,
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/CompositeValueProviderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/CompositeValueProviderTest.cs
index 43b49ce16c..cdf2f80b47 100644
--- a/src/Mvc/Mvc.Core/test/ModelBinding/CompositeValueProviderTest.cs
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/CompositeValueProviderTest.cs
@@ -5,7 +5,10 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
@@ -45,6 +48,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new CompositeValueProvider() { emptyValueProvider, valueProvider };
}
+ [Fact]
+ public async Task TryCreateAsync_AddsModelStateError_WhenValueProviderFactoryThrowsValueProviderException()
+ {
+ // Arrange
+ var factory = new Mock();
+ factory.Setup(f => f.CreateValueProviderAsync(It.IsAny())).ThrowsAsync(new ValueProviderException("Some error"));
+ var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor(), new ModelStateDictionary());
+
+ // Act
+ var (success, result) = await CompositeValueProvider.TryCreateAsync(actionContext, new[] { factory.Object });
+
+ // Assert
+ Assert.False(success);
+ var modelState = actionContext.ModelState;
+ Assert.False(modelState.IsValid);
+ var entry = Assert.Single(modelState);
+ Assert.Empty(entry.Key);
+ }
+
[Fact]
public void GetKeysFromPrefixAsync_ReturnsResultFromFirstValueProviderThatReturnsValues()
{
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs
index c0030a0ba5..9926fc1934 100644
--- a/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormFileValueProviderFactoryTest.cs
@@ -1,13 +1,16 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
+using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -60,6 +63,59 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
v => Assert.IsType(v));
}
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidDataException()
+ {
+ // Arrange
+ var exception = new InvalidDataException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new FormFileValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidOperationException()
+ {
+ // Arrange
+ var exception = new IOException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new FormFileValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsOriginalException_IfReadingFormThrows()
+ {
+ // Arrange
+ var exception = new TimeZoneNotFoundException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new FormFileValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex);
+ }
+
+ private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
+ {
+ var context = new Mock();
+ context.Setup(c => c.Request.ContentType).Returns("application/x-www-form-urlencoded");
+ context.Setup(c => c.Request.HasFormContentType).Returns(true);
+ context.Setup(c => c.Request.ReadFormAsync(It.IsAny())).ThrowsAsync(exception);
+ var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor());
+ var valueProviderContext = new ValueProviderFactoryContext(actionContext);
+ return valueProviderContext;
+ }
+
private static ValueProviderFactoryContext CreateContext(string contentType)
{
var context = new DefaultHttpContext();
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderFactoryTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderFactoryTest.cs
index 09aff4b206..53873b0ee6 100644
--- a/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderFactoryTest.cs
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/FormValueProviderFactoryTest.cs
@@ -1,13 +1,17 @@
// 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.Globalization;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
+using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
@@ -47,6 +51,59 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal(CultureInfo.CurrentCulture, valueProvider.Culture);
}
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidDataException()
+ {
+ // Arrange
+ var exception = new InvalidDataException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new FormValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidOperationException()
+ {
+ // Arrange
+ var exception = new IOException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new FormValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsOriginalException_IfReadingFormThrows()
+ {
+ // Arrange
+ var exception = new TimeZoneNotFoundException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new FormValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex);
+ }
+
+ private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
+ {
+ var context = new Mock();
+ context.Setup(c => c.Request.ContentType).Returns("application/x-www-form-urlencoded");
+ context.Setup(c => c.Request.HasFormContentType).Returns(true);
+ context.Setup(c => c.Request.ReadFormAsync(It.IsAny())).ThrowsAsync(exception);
+ var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor());
+ var valueProviderContext = new ValueProviderFactoryContext(actionContext);
+ return valueProviderContext;
+ }
+
private static ValueProviderFactoryContext CreateContext(string contentType)
{
var context = new DefaultHttpContext();
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderFactoryTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
index d940134a8d..186b04c64e 100644
--- a/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/JQueryFormValueProviderFactoryTest.cs
@@ -1,13 +1,17 @@
// 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.Globalization;
+using System.IO;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
+using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
@@ -132,6 +136,59 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Test
Assert.Equal(CultureInfo.CurrentCulture, jqueryFormValueProvider.Culture);
}
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidDataException()
+ {
+ // Arrange
+ var exception = new InvalidDataException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new JQueryFormValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsValueProviderException_IfReadingFormThrowsInvalidOperationException()
+ {
+ // Arrange
+ var exception = new IOException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new JQueryFormValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex.InnerException);
+ }
+
+ [Fact]
+ public async Task GetValueProviderAsync_ThrowsOriginalException_IfReadingFormThrows()
+ {
+ // Arrange
+ var exception = new TimeZoneNotFoundException();
+ var valueProviderContext = CreateThrowingContext(exception);
+
+ var factory = new JQueryFormValueProviderFactory();
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync(() => factory.CreateValueProviderAsync(valueProviderContext));
+ Assert.Same(exception, ex);
+ }
+
+ private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
+ {
+ var context = new Mock();
+ context.Setup(c => c.Request.ContentType).Returns("application/x-www-form-urlencoded");
+ context.Setup(c => c.Request.HasFormContentType).Returns(true);
+ context.Setup(c => c.Request.ReadFormAsync(It.IsAny())).ThrowsAsync(exception);
+ var actionContext = new ActionContext(context.Object, new RouteData(), new ActionDescriptor());
+ var valueProviderContext = new ValueProviderFactoryContext(actionContext);
+ return valueProviderContext;
+ }
+
private static ValueProviderFactoryContext CreateContext(string contentType, Dictionary formValues)
{
var context = new DefaultHttpContext();
diff --git a/src/Mvc/test/Mvc.FunctionalTests/ReadFromDisconnectedClientTest.cs b/src/Mvc/test/Mvc.FunctionalTests/ReadFromDisconnectedClientTest.cs
new file mode 100644
index 0000000000..869e4607eb
--- /dev/null
+++ b/src/Mvc/test/Mvc.FunctionalTests/ReadFromDisconnectedClientTest.cs
@@ -0,0 +1,63 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.FunctionalTests
+{
+ // These tests verify the behavior of MVC when responding to a client that simulates a disconnect.
+ // See https://github.com/dotnet/aspnetcore/issues/13333
+ public class ReadFromDisconnectedClientTest : IClassFixture>
+ {
+ public ReadFromDisconnectedClientTest(MvcTestFixture fixture)
+ {
+ var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
+ Client = factory.CreateDefaultClient();
+ }
+
+ private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
+ builder.UseStartup();
+
+ public HttpClient Client { get; }
+
+ [Fact]
+ public async Task ActionWithAntiforgery_Returns400_WhenReadingBodyThrows()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Post, "ReadFromThrowingRequestBody/AppliesAntiforgeryValidation");
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+ }
+
+ [Fact]
+ public async Task ActionReadingForm_ReturnsInvalidModelState_WhenReadingBodyThrows()
+ {
+ // Arrange
+ var request = new HttpRequestMessage(HttpMethod.Post, "ReadFromThrowingRequestBody/ReadForm");
+ request.Content = new FormUrlEncodedContent(new Dictionary
+ {
+ ["key"] = "value",
+ });
+
+ // Act
+ var response = await Client.SendAsync(request);
+
+ // Assert
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
+ var problem = await response.Content.ReadFromJsonAsync();
+ var error = Assert.Single(problem.Errors);
+ Assert.Empty(error.Key);
+ }
+ }
+}
diff --git a/src/Mvc/test/Mvc.FunctionalTests/RequestFormLimitsTest.cs b/src/Mvc/test/Mvc.FunctionalTests/RequestFormLimitsTest.cs
index 285b6cc397..1a9c1a8f38 100644
--- a/src/Mvc/test/Mvc.FunctionalTests/RequestFormLimitsTest.cs
+++ b/src/Mvc/test/Mvc.FunctionalTests/RequestFormLimitsTest.cs
@@ -1,11 +1,10 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
-using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Xunit;
@@ -26,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public HttpClient Client { get; }
[Fact]
- public async Task RequestFormLimitCheckHappens_BeforeAntiforgeryTokenValidation()
+ public async Task RequestFormLimitCheckHappens_WithAntiforgeryValidation()
{
// Arrange
var request = new HttpRequestMessage();
@@ -43,11 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
new FormUrlEncodedContent(kvps));
// Assert
- Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
- var result = await response.Content.ReadAsStringAsync();
- Assert.Contains(
- "InvalidDataException: Form value count limit 2 exceeded.",
- result);
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
}
[Fact]
@@ -103,7 +98,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task RequestSizeLimitCheckHappens_BeforeRequestFormLimits()
{
// Arrange
- var request = new HttpRequestMessage();
var kvps = new List>();
// Request size has a limit of 100 bytes
// Request form limits has a value count limit of 2
@@ -129,7 +123,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task RequestFormLimitsCheckHappens_AfterRequestSizeLimit()
{
// Arrange
- var request = new HttpRequestMessage();
var kvps = new List>();
// Request size has a limit of 100 bytes
// Request form limits has a value count limit of 2
@@ -145,11 +138,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
new FormUrlEncodedContent(kvps));
// Assert
- Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
- var result = await response.Content.ReadAsStringAsync();
- Assert.Contains(
- "InvalidDataException: Form value count limit 2 exceeded.",
- result);
+ await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
}
[Fact]
diff --git a/src/Mvc/test/WebSites/BasicWebSite/Controllers/ReadFromThrowingRequestBodyController .cs b/src/Mvc/test/WebSites/BasicWebSite/Controllers/ReadFromThrowingRequestBodyController .cs
new file mode 100644
index 0000000000..07c2c04e48
--- /dev/null
+++ b/src/Mvc/test/WebSites/BasicWebSite/Controllers/ReadFromThrowingRequestBodyController .cs
@@ -0,0 +1,33 @@
+// 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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace BasicWebSite.Controllers
+{
+ public class ReadFromThrowingRequestBodyController : Controller
+ {
+ [ValidateAntiForgeryToken]
+ [HttpPost]
+ public IActionResult AppliesAntiforgeryValidation() => Ok();
+
+ [HttpPost]
+ public IActionResult ReadForm(Person person, IFormFile form)
+ {
+ if (!ModelState.IsValid)
+ {
+ return ValidationProblem();
+ }
+
+ return Ok();
+ }
+
+ public class Person
+ {
+ public string Name { get; set; }
+
+ public int Age { get; set; }
+ }
+ }
+}
diff --git a/src/Mvc/test/WebSites/BasicWebSite/StartupWhereReadingRequestBodyThrows.cs b/src/Mvc/test/WebSites/BasicWebSite/StartupWhereReadingRequestBodyThrows.cs
new file mode 100644
index 0000000000..25b3855d2e
--- /dev/null
+++ b/src/Mvc/test/WebSites/BasicWebSite/StartupWhereReadingRequestBodyThrows.cs
@@ -0,0 +1,89 @@
+// 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BasicWebSite
+{
+ public class StartupWhereReadingRequestBodyThrows
+ {
+ // Set up application services
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddControllersWithViews()
+ .SetCompatibilityVersion(CompatibilityVersion.Latest);
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseDeveloperExceptionPage();
+
+ // Initializes the RequestId service for each request
+ app.Use((context, next) =>
+ {
+ context.Request.Body = new ThrowingStream();
+ return next();
+ });
+
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapDefaultControllerRoute();
+ });
+ }
+
+ private class ThrowingStream : Stream
+ {
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => false;
+ public override long Length => throw new NotSupportedException();
+ public override long Position { get; set; }
+
+ public override void Flush()
+ {
+ throw new NotSupportedException();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ throw new ConnectionResetException("Some error");
+ }
+
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
+ {
+ throw new ConnectionResetException("Some error");
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+ }
+ }
+}