diff --git a/src/Http/Http/src/Features/FormFeature.cs b/src/Http/Http/src/Features/FormFeature.cs index 952e57f12f07..4d9ecda48b22 100644 --- a/src/Http/Http/src/Features/FormFeature.cs +++ b/src/Http/Http/src/Features/FormFeature.cs @@ -251,9 +251,11 @@ private async Task InnerReadFormAsync(CancellationToken cancell var fileSection = new FileMultipartSection(section, contentDisposition); // Enable buffering for the file if not already done for the full body + // Only first file can be a reference to buffered body to avoid concurrency issues section.EnableRewind( _request.HttpContext.Response.RegisterForDispose, - _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit); + _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit, + forceBuffering: files is not null); // Find the end await section.Body.DrainAsync(cancellationToken); diff --git a/src/Http/Http/src/Internal/BufferingHelper.cs b/src/Http/Http/src/Internal/BufferingHelper.cs index 8b407c8d2376..2a878184acdb 100644 --- a/src/Http/Http/src/Internal/BufferingHelper.cs +++ b/src/Http/Http/src/Internal/BufferingHelper.cs @@ -25,16 +25,17 @@ public static HttpRequest EnableRewind(this HttpRequest request, int bufferThres } public static MultipartSection EnableRewind(this MultipartSection section, Action registerForDispose, - int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null, bool forceBuffering = false) { ArgumentNullException.ThrowIfNull(section); ArgumentNullException.ThrowIfNull(registerForDispose); var body = section.Body; - if (!body.CanSeek) + if (!body.CanSeek || forceBuffering) { var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory); section.Body = fileStream; + section.BaseStreamOffset = null; registerForDispose(fileStream); } return section; diff --git a/src/Http/Http/test/Features/FormFeatureTests.cs b/src/Http/Http/test/Features/FormFeatureTests.cs index e3ab9b1b76fe..9137203a0b07 100644 --- a/src/Http/Http/test/Features/FormFeatureTests.cs +++ b/src/Http/Http/test/Features/FormFeatureTests.cs @@ -176,6 +176,11 @@ private class MockRequestBodyPipeFeature : IRequestBodyPipeFeature MultipartFormFile + MultipartFormEnd; + private const string MultipartFormWithFiles = + MultipartFormFile + + MultipartFormFile + + MultipartFormEnd; + private const string MultipartFormWithFieldAndFile = MultipartFormField + MultipartFormFile + @@ -313,6 +318,38 @@ public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection(bo await responseFeature.CompleteAsync(); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFiles_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFiles); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + Assert.NotNull(formCollection.Files); + Assert.Equal(2, formCollection.Files.Count); + + Stream[] streams = [formCollection.Files[0].OpenReadStream(), formCollection.Files[1].OpenReadStream()]; + foreach (var stream in streams.Reverse()) + { + using var reader = new StreamReader(stream); + Assert.True(stream.CanSeek); + await reader.ReadToEndAsync(); + } + + await responseFeature.CompleteAsync(); + } + [Theory] [InlineData(true)] [InlineData(false)]