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:
Pranav K 2020-08-25 09:29:50 -07:00 committed by GitHub
parent a9b596e091
commit 78a587b02e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 493 additions and 19 deletions

View File

@ -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];
}

View File

@ -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>

View File

@ -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)]

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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()
{

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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);
}
}
}

View File

@ -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]

View File

@ -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; }
}
}
}

View File

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