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(); + } + } + } +}