From 3520d0bba00cc29a28d9d6df405be20d547a232b Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 5 Sep 2026 09:36:46 +1000 Subject: [PATCH 1/2] Add Read(Span) override to MultipartReaderStream Move the synchronous read implementation onto Read(Span) and have the array overload delegate to it, so a caller reading a section into a span does not pay the base Stream fallback's rented array and copy. Also add the throwing Write(ReadOnlySpan) to match the other write overrides. --- src/Http/WebUtilities/src/MultipartReaderStream.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Http/WebUtilities/src/MultipartReaderStream.cs b/src/Http/WebUtilities/src/MultipartReaderStream.cs index 80297beec433..db8c9f8ce7fa 100644 --- a/src/Http/WebUtilities/src/MultipartReaderStream.cs +++ b/src/Http/WebUtilities/src/MultipartReaderStream.cs @@ -116,6 +116,11 @@ public override void Write(byte[] buffer, int offset, int count) throw new NotSupportedException(); } + public override void Write(ReadOnlySpan buffer) + { + throw new NotSupportedException(); + } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { throw new NotSupportedException(); @@ -164,6 +169,9 @@ private int UpdatePosition(int read) } public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) { if (_finished) { @@ -184,7 +192,7 @@ public override int Read(byte[] buffer, int offset, int count) if (index != 0) { // Sync, it's already buffered - var slice = buffer.AsSpan(offset, Math.Min(count, index)); + var slice = buffer[..Math.Min(buffer.Length, index)]; var readAmount = _innerStream.Read(slice); return UpdatePosition(readAmount); @@ -204,7 +212,7 @@ public override int Read(byte[] buffer, int offset, int count) // We found a possible match, return any data before it. if (matchOffset > bufferedData.Offset) { - read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + read = _innerStream.Read(buffer[..Math.Min(buffer.Length, matchOffset - bufferedData.Offset)]); return UpdatePosition(read); } @@ -215,7 +223,7 @@ public override int Read(byte[] buffer, int offset, int count) } // No possible boundary match within the buffered data, return the data from the buffer. - read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + read = _innerStream.Read(buffer[..Math.Min(buffer.Length, bufferedData.Count)]); return UpdatePosition(read); static int ReadBoundary(MultipartReaderStream stream, int length) From ffad09a7dd7790cd2222eb4006ee4601fa476ff5 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 5 Sep 2026 09:56:15 +1000 Subject: [PATCH 2/2] Add test reading multipart section bodies through Read(Span) Read each section of a two part body into a span smaller than the section, with both the default buffer and one smaller than the body so the partial boundary match path is exercised as well. --- .../WebUtilities/test/MultipartReaderTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Http/WebUtilities/test/MultipartReaderTests.cs b/src/Http/WebUtilities/test/MultipartReaderTests.cs index de2396cbfde8..16258e40724d 100644 --- a/src/Http/WebUtilities/test/MultipartReaderTests.cs +++ b/src/Http/WebUtilities/test/MultipartReaderTests.cs @@ -93,6 +93,18 @@ public class MultipartReaderTests "\r\n" + "--9051914041544843365"; + private static string ReadToEndUsingSpans(Stream stream, int spanLength) + { + var buffer = new byte[spanLength]; + var result = new MemoryStream(); + int read; + while ((read = stream.Read(buffer.AsSpan())) > 0) + { + result.Write(buffer, 0, read); + } + return Encoding.ASCII.GetString(result.ToArray()); + } + private static MemoryStream MakeStream(string text) { return new MemoryStream(Encoding.UTF8.GetBytes(text)); @@ -502,6 +514,29 @@ public async Task SyncReadWithOffsetWorks() Assert.Null(await reader.ReadNextSectionAsync()); } + [Theory] + // The default buffer holds the whole body, so every read sees the full boundary. + [InlineData(4096)] + // A buffer smaller than the body forces partial boundary matches at the end of the buffered data. + [InlineData(64)] + public async Task MultipartReader_ReadSectionBodyIntoSpan_ReadsEachSection(int bufferSize) + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream, bufferSize); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text default", ReadToEndUsingSpans(section.Body, spanLength: 5)); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("Content of a.txt.\r\n", ReadToEndUsingSpans(section.Body, spanLength: 5)); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + [Fact] public async Task MultipartReader_BoundaryWithUnexpectedTrailingData_ThrowsIOException() {