Guard against client disconnect exceptions that appear when reading body (#25146)
* Guard against client disconnect exceptions that appear when performing ReadFormAsync Reading the request body may throw an exception. This change adds some extra guards for this and presents this as a 4xx response rather than a 5xx response. * Add some tests * Fixup test
This commit is contained in:
parent
a9b596e091
commit
78a587b02e
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -136,6 +136,9 @@
|
|||
<data name="AntiforgeryToken_TokensSwapped" xml:space="preserve">
|
||||
<value>Validation of the provided antiforgery token failed. The cookie token and the request token were swapped.</value>
|
||||
</data>
|
||||
<data name="AntiforgeryToken_UnableToReadRequest" xml:space="preserve">
|
||||
<value>Unable to read the antiforgery request token from the posted form.</value>
|
||||
</data>
|
||||
<data name="AntiforgeryToken_UsernameMismatch" xml:space="preserve">
|
||||
<value>The provided antiforgery token was meant for user "{0}", but the current user is "{1}".</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
httpContext.Setup(r => r.Request.Cookies).Returns(Mock.Of<IRequestCookieCollection>());
|
||||
httpContext.SetupGet(r => r.Request.HasFormContentType).Returns(true);
|
||||
httpContext.Setup(r => r.Request.ReadFormAsync(It.IsAny<CancellationToken>())).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<AntiforgeryValidationException>(() => 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>();
|
||||
|
||||
httpContext.Setup(r => r.Request.Cookies).Returns(Mock.Of<IRequestCookieCollection>());
|
||||
httpContext.SetupGet(r => r.Request.HasFormContentType).Returns(true);
|
||||
httpContext.Setup(r => r.Request.ReadFormAsync(It.IsAny<CancellationToken>())).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<AntiforgeryValidationException>(() => tokenStore.GetRequestTokensAsync(httpContext.Object));
|
||||
Assert.Same(exception, ex.InnerException);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, CookieSecurePolicy.SameAsRequest, null)]
|
||||
[InlineData(true, CookieSecurePolicy.SameAsRequest, true)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<IValueProviderFactory>();
|
||||
factory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>())).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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<FormFileValueProvider>(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<ValueProviderException>(() => 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<ValueProviderException>(() => 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<TimeZoneNotFoundException>(() => factory.CreateValueProviderAsync(valueProviderContext));
|
||||
Assert.Same(exception, ex);
|
||||
}
|
||||
|
||||
private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
|
||||
{
|
||||
var context = new Mock<HttpContext>();
|
||||
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<CancellationToken>())).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();
|
||||
|
|
|
|||
|
|
@ -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<ValueProviderException>(() => 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<ValueProviderException>(() => 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<TimeZoneNotFoundException>(() => factory.CreateValueProviderAsync(valueProviderContext));
|
||||
Assert.Same(exception, ex);
|
||||
}
|
||||
|
||||
private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
|
||||
{
|
||||
var context = new Mock<HttpContext>();
|
||||
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<CancellationToken>())).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();
|
||||
|
|
|
|||
|
|
@ -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<ValueProviderException>(() => 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<ValueProviderException>(() => 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<TimeZoneNotFoundException>(() => factory.CreateValueProviderAsync(valueProviderContext));
|
||||
Assert.Same(exception, ex);
|
||||
}
|
||||
|
||||
private static ValueProviderFactoryContext CreateThrowingContext(Exception exception)
|
||||
{
|
||||
var context = new Mock<HttpContext>();
|
||||
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<CancellationToken>())).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<string, StringValues> formValues)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
|
|
|||
|
|
@ -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<MvcTestFixture<BasicWebSite.StartupWhereReadingRequestBodyThrows>>
|
||||
{
|
||||
public ReadFromDisconnectedClientTest(MvcTestFixture<BasicWebSite.StartupWhereReadingRequestBodyThrows> fixture)
|
||||
{
|
||||
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
|
||||
Client = factory.CreateDefaultClient();
|
||||
}
|
||||
|
||||
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
|
||||
builder.UseStartup<BasicWebSite.StartupWhereReadingRequestBodyThrows>();
|
||||
|
||||
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<string, string>
|
||||
{
|
||||
["key"] = "value",
|
||||
});
|
||||
|
||||
// Act
|
||||
var response = await Client.SendAsync(request);
|
||||
|
||||
// Assert
|
||||
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
|
||||
var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
|
||||
var error = Assert.Single(problem.Errors);
|
||||
Assert.Empty(error.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<KeyValuePair<string, string>>();
|
||||
// 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<KeyValuePair<string, string>>();
|
||||
// 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]
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new ConnectionResetException("Some error");
|
||||
}
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue