From 15f594382a1eedd2a9d3aadf0e44be68de3085a0 Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 4 Mar 2026 17:42:29 +0100 Subject: [PATCH 1/6] BufferedReadStream process multiple bytes at once in ReadLineAsync --- .../BufferedReadStream.cs | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs index 144c7c2c..f6d4efc3 100644 --- a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -144,15 +144,14 @@ public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available public async Task ReadLineAsync(CancellationToken cancellationToken) { - var line = new StringBuilder(32); + using var memoryStream = new MemoryStream(); - var crIndex = -1; + const byte cr = (byte)'\r'; + const byte lf = (byte)'\n'; - var lfIndex = -1; + bool crFound = false; - bool crlfFound; - - do + while (true) { if (_bufferCount == 0) { @@ -160,29 +159,55 @@ public async Task ReadLineAsync(CancellationToken cancellationToken) _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) .ConfigureAwait(false); - } - - var c = (char)_buffer[_bufferOffset]; - line.Append(c); - _bufferOffset++; - _bufferCount--; + if (_bufferCount == 0) + { + return null; + } + } - switch (c) + if (crFound) { - case '\r': - crIndex = line.Length; - break; - case '\n': - lfIndex = line.Length; + if (_buffer[_bufferOffset] == lf) + { + _bufferOffset += 1; + _bufferCount -= 1; break; + } + crFound = false; + memoryStream.WriteByte(cr); } - crlfFound = crIndex + 1 == lfIndex; + var crIndex = _buffer.AsSpan(_bufferOffset, _bufferCount).IndexOf(cr); + if (crIndex != -1) + { + memoryStream.Write(_buffer, _bufferOffset, crIndex); + _bufferOffset += crIndex + 1; + _bufferCount -= crIndex + 1; + + if (_bufferCount > 0) + { + if (_buffer[_bufferOffset] == lf) + { + _bufferOffset++; + _bufferCount--; + break; + } + memoryStream.WriteByte(cr); + } + else + { + crFound = true; + } + } + else + { + memoryStream.Write(_buffer, _bufferOffset, _bufferCount); + _bufferCount = 0; + } } - while (!crlfFound); - return line.ToString(0, line.Length - 2); + return Encoding.ASCII.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); } private int ReadBuffer(byte[] buffer, int offset, int count) @@ -205,7 +230,7 @@ private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint ava { int toCopy = Math.Min(_bufferCount, (int)toPeek); Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); - peeked = (uint) toCopy; + peeked = (uint)toCopy; available = (uint)_bufferCount; remaining = available - peeked; return toCopy; From 27079fe3e77e95560aab0d81926f7e6e29fba83e Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 4 Mar 2026 17:43:29 +0100 Subject: [PATCH 2/6] Reuse MemoryStream during line processing --- .../BufferedReadStream.cs | 4 +- .../ChunkedReadStream.cs | 64 +++++++++++-------- .../HttpConnection.cs | 3 +- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs index f6d4efc3..2e85ebf5 100644 --- a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -142,9 +142,9 @@ public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available throw new NotSupportedException("_inner stream isn't a peekable stream"); } - public async Task ReadLineAsync(CancellationToken cancellationToken) + public async Task ReadLineAsync(MemoryStream memoryStream, CancellationToken cancellationToken) { - using var memoryStream = new MemoryStream(); + memoryStream.SetLength(0); const byte cr = (byte)'\r'; const byte lf = (byte)'\n'; diff --git a/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs index f127a79b..3fa9835c 100644 --- a/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -59,48 +59,56 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, return 0; } - if (_chunkBytesRemaining == 0) + MemoryStream memoryStream = null; + try { - var headerLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + if (_chunkBytesRemaining == 0) { - throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); + var headerLine = await _inner.ReadLineAsync(memoryStream ??= new MemoryStream(), cancellationToken) + .ConfigureAwait(false); + + if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + { + throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); + } } - } - var readBytesCount = 0; + var readBytesCount = 0; - if (_chunkBytesRemaining > 0) - { - var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); + if (_chunkBytesRemaining > 0) + { + var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); - readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) - .ConfigureAwait(false); + readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); - if (readBytesCount == 0) - { - throw new EndOfStreamException(); + if (readBytesCount == 0) + { + throw new EndOfStreamException(); + } + + _chunkBytesRemaining -= readBytesCount; } - _chunkBytesRemaining -= readBytesCount; - } + if (_chunkBytesRemaining == 0) + { + var emptyLine = await _inner.ReadLineAsync(memoryStream ??= new MemoryStream(), cancellationToken) + .ConfigureAwait(false); - if (_chunkBytesRemaining == 0) - { - var emptyLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); + if (!string.IsNullOrEmpty(emptyLine)) + { + throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); + } - if (!string.IsNullOrEmpty(emptyLine)) - { - throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); + _done = readBytesCount == 0; } - _done = readBytesCount == 0; + return readBytesCount; + } + finally + { + memoryStream?.Dispose(); } - - return readBytesCount; } public override long Seek(long offset, SeekOrigin origin) diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index fb8c1ab2..3d55b953 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -114,11 +114,12 @@ private static void AppendHeaders(StringBuilder builder, HttpHeaders headers) private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) { + using var memoryStream = new MemoryStream(); var lines = new List(12); do { - var line = await Transport.ReadLineAsync(cancellationToken) + var line = await Transport.ReadLineAsync(memoryStream, cancellationToken) .ConfigureAwait(false); if (string.IsNullOrEmpty(line)) From 95cd0b102bfceaaf8776e1ac3c6442e6640fa2ef Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 4 Mar 2026 20:57:37 +0100 Subject: [PATCH 3/6] PR feedback --- src/Microsoft.Net.Http.Client/ChunkedReadStream.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs index 3fa9835c..bf2928fe 100644 --- a/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -64,7 +64,7 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, { if (_chunkBytesRemaining == 0) { - var headerLine = await _inner.ReadLineAsync(memoryStream ??= new MemoryStream(), cancellationToken) + var headerLine = await _inner.ReadLineAsync(memoryStream = new MemoryStream(), cancellationToken) .ConfigureAwait(false); if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) @@ -95,7 +95,12 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, var emptyLine = await _inner.ReadLineAsync(memoryStream ??= new MemoryStream(), cancellationToken) .ConfigureAwait(false); - if (!string.IsNullOrEmpty(emptyLine)) + if (emptyLine == null) + { + throw new EndOfStreamException(); + } + + if (emptyLine.Length > 0) { throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); } From b0ad8440b38fd40cb836ba09a3ed6cea3a429566 Mon Sep 17 00:00:00 2001 From: campersau Date: Wed, 4 Mar 2026 21:01:56 +0100 Subject: [PATCH 4/6] ++ -- --- src/Microsoft.Net.Http.Client/BufferedReadStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs index 2e85ebf5..6c24ce76 100644 --- a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -170,8 +170,8 @@ public async Task ReadLineAsync(MemoryStream memoryStream, CancellationT { if (_buffer[_bufferOffset] == lf) { - _bufferOffset += 1; - _bufferCount -= 1; + _bufferOffset++; + _bufferCount--; break; } crFound = false; From 9e56b8313234fd43769550dd8e752bf2d2812c8b Mon Sep 17 00:00:00 2001 From: campersau Date: Thu, 5 Mar 2026 09:32:07 +0100 Subject: [PATCH 5/6] Move read line buffer to BufferedReadStream --- .../BufferedReadStream.cs | 25 +++++-- .../ChunkedReadStream.cs | 72 +++++++++---------- .../HttpConnection.cs | 3 +- 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs index 6c24ce76..82d299d0 100644 --- a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -16,6 +16,8 @@ internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream private int _bufferCount; + private MemoryStream _readLineBuffer; + public BufferedReadStream(Stream inner, Socket socket, ILogger logger) : this(inner, socket, 8192, logger) { @@ -63,6 +65,8 @@ protected override void Dispose(bool disposing) ArrayPool.Shared.Return(_buffer); } + _readLineBuffer?.Dispose(); + _inner.Dispose(); } @@ -142,9 +146,16 @@ public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available throw new NotSupportedException("_inner stream isn't a peekable stream"); } - public async Task ReadLineAsync(MemoryStream memoryStream, CancellationToken cancellationToken) + public async Task ReadLineAsync(CancellationToken cancellationToken) { - memoryStream.SetLength(0); + if (_readLineBuffer == null) + { + _readLineBuffer = new MemoryStream(); + } + else + { + _readLineBuffer.SetLength(0); + } const byte cr = (byte)'\r'; const byte lf = (byte)'\n'; @@ -175,13 +186,13 @@ public async Task ReadLineAsync(MemoryStream memoryStream, CancellationT break; } crFound = false; - memoryStream.WriteByte(cr); + _readLineBuffer.WriteByte(cr); } var crIndex = _buffer.AsSpan(_bufferOffset, _bufferCount).IndexOf(cr); if (crIndex != -1) { - memoryStream.Write(_buffer, _bufferOffset, crIndex); + _readLineBuffer.Write(_buffer, _bufferOffset, crIndex); _bufferOffset += crIndex + 1; _bufferCount -= crIndex + 1; @@ -193,7 +204,7 @@ public async Task ReadLineAsync(MemoryStream memoryStream, CancellationT _bufferCount--; break; } - memoryStream.WriteByte(cr); + _readLineBuffer.WriteByte(cr); } else { @@ -202,12 +213,12 @@ public async Task ReadLineAsync(MemoryStream memoryStream, CancellationT } else { - memoryStream.Write(_buffer, _bufferOffset, _bufferCount); + _readLineBuffer.Write(_buffer, _bufferOffset, _bufferCount); _bufferCount = 0; } } - return Encoding.ASCII.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length); + return Encoding.ASCII.GetString(_readLineBuffer.GetBuffer(), 0, (int)_readLineBuffer.Length); } private int ReadBuffer(byte[] buffer, int offset, int count) diff --git a/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs index bf2928fe..e4e26346 100644 --- a/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -59,61 +59,53 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, return 0; } - MemoryStream memoryStream = null; - try + if (_chunkBytesRemaining == 0) { - if (_chunkBytesRemaining == 0) - { - var headerLine = await _inner.ReadLineAsync(memoryStream = new MemoryStream(), cancellationToken) - .ConfigureAwait(false); + var headerLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); - if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) - { - throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); - } + if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + { + throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); } + } - var readBytesCount = 0; - - if (_chunkBytesRemaining > 0) - { - var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); + var readBytesCount = 0; - readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) - .ConfigureAwait(false); + if (_chunkBytesRemaining > 0) + { + var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); - if (readBytesCount == 0) - { - throw new EndOfStreamException(); - } + readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); - _chunkBytesRemaining -= readBytesCount; + if (readBytesCount == 0) + { + throw new EndOfStreamException(); } - if (_chunkBytesRemaining == 0) - { - var emptyLine = await _inner.ReadLineAsync(memoryStream ??= new MemoryStream(), cancellationToken) - .ConfigureAwait(false); + _chunkBytesRemaining -= readBytesCount; + } - if (emptyLine == null) - { - throw new EndOfStreamException(); - } + if (_chunkBytesRemaining == 0) + { + var emptyLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); - if (emptyLine.Length > 0) - { - throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); - } + if (emptyLine == null) + { + throw new EndOfStreamException(); + } - _done = readBytesCount == 0; + if (emptyLine.Length > 0) + { + throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); } - return readBytesCount; - } - finally - { - memoryStream?.Dispose(); + _done = readBytesCount == 0; } + + return readBytesCount; } public override long Seek(long offset, SeekOrigin origin) diff --git a/src/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs index 3d55b953..fb8c1ab2 100644 --- a/src/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -114,12 +114,11 @@ private static void AppendHeaders(StringBuilder builder, HttpHeaders headers) private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) { - using var memoryStream = new MemoryStream(); var lines = new List(12); do { - var line = await Transport.ReadLineAsync(memoryStream, cancellationToken) + var line = await Transport.ReadLineAsync(cancellationToken) .ConfigureAwait(false); if (string.IsNullOrEmpty(line)) From 9e467b20d2c35d22e19491f6b4b0087eb18622ad Mon Sep 17 00:00:00 2001 From: campersau Date: Fri, 6 Mar 2026 08:55:58 +0100 Subject: [PATCH 6/6] Return what is left in _readLineBuffer at the end --- src/Microsoft.Net.Http.Client/BufferedReadStream.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs index 82d299d0..64357bb8 100644 --- a/src/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -173,7 +173,17 @@ public async Task ReadLineAsync(CancellationToken cancellationToken) if (_bufferCount == 0) { - return null; + if (crFound) + { + _readLineBuffer.WriteByte(cr); + } + + if (_readLineBuffer.Length == 0) + { + return null; + } + + break; // return what is left in _readLineBuffer } }