Skip to content

Commit 6d90a04

Browse files
authored
Default to zero byte reads when we use PipeReader.Create (#43276)
* Default to zero byte reads when we use PipeReader.Create - This will avoid allocating buffers until there's data available to read.
1 parent 4faa91c commit 6d90a04

File tree

6 files changed

+81
-7
lines changed

6 files changed

+81
-7
lines changed

src/Http/Http/src/Features/RequestBodyPipeFeature.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ public class RequestBodyPipeFeature : IRequestBodyPipeFeature
1414
private Stream? _streamInstanceWhenWrapped;
1515
private readonly HttpContext _context;
1616

17+
// We want to use zero byte reads for the request body
18+
private static readonly StreamPipeReaderOptions _defaultReaderOptions = new(useZeroByteReads: true);
19+
1720
/// <summary>
1821
/// Initializes a new instance of <see cref="IRequestBodyPipeFeature"/>.
1922
/// </summary>
@@ -36,7 +39,7 @@ public PipeReader Reader
3639
!ReferenceEquals(_streamInstanceWhenWrapped, _context.Request.Body))
3740
{
3841
_streamInstanceWhenWrapped = _context.Request.Body;
39-
_internalPipeReader = PipeReader.Create(_context.Request.Body);
42+
_internalPipeReader = PipeReader.Create(_context.Request.Body, _defaultReaderOptions);
4043

4144
_context.Response.OnCompleted((self) =>
4245
{

src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Buffers;
55
using System.IO.Pipelines;
66
using System.Text;
7+
using Moq;
78

89
namespace Microsoft.AspNetCore.Http.Features;
910

@@ -37,6 +38,30 @@ public async Task RequestBodyGetsDataFromSecondStream()
3738
Assert.Equal(expectedString, GetStringFromReadResult(data));
3839
}
3940

41+
[Fact]
42+
public async Task RequestBodyDoesZeroByteRead()
43+
{
44+
var context = new DefaultHttpContext();
45+
var mockStream = new Mock<Stream>();
46+
47+
var bufferLengths = new List<int>();
48+
49+
mockStream.Setup(s => s.CanRead).Returns(true);
50+
mockStream.Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>())).Returns<Memory<byte>, CancellationToken>((buffer, token) =>
51+
{
52+
bufferLengths.Add(buffer.Length);
53+
return ValueTask.FromResult(0);
54+
});
55+
56+
context.Request.Body = mockStream.Object;
57+
var feature = new RequestBodyPipeFeature(context);
58+
var data = await feature.Reader.ReadAsync();
59+
60+
Assert.Equal(2, bufferLengths.Count);
61+
Assert.Equal(0, bufferLengths[0]);
62+
Assert.Equal(4096, bufferLengths[1]);
63+
}
64+
4065
private static string GetStringFromReadResult(ReadResult data)
4166
{
4267
return Encoding.ASCII.GetString(data.Buffer.ToArray());

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.FeatureCollection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ PipeReader IRequestBodyPipeFeature.Reader
9292
if (!ReferenceEquals(_requestStreamInternal, RequestBody))
9393
{
9494
_requestStreamInternal = RequestBody;
95-
RequestBodyPipeReader = PipeReader.Create(RequestBody, new StreamPipeReaderOptions(_context.MemoryPool, _context.MemoryPool.GetMinimumSegmentSize(), _context.MemoryPool.GetMinimumAllocSize()));
95+
RequestBodyPipeReader = PipeReader.Create(RequestBody, new StreamPipeReaderOptions(_context.MemoryPool, _context.MemoryPool.GetMinimumSegmentSize(), _context.MemoryPool.GetMinimumAllocSize(), useZeroByteReads: true));
9696

9797
OnCompleted((self) =>
9898
{

src/Servers/Kestrel/Core/src/Middleware/Internal/LoggingStream.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,28 +76,40 @@ public override Task FlushAsync(CancellationToken cancellationToken)
7676
public override int Read(byte[] buffer, int offset, int count)
7777
{
7878
int read = _inner.Read(buffer, offset, count);
79-
Log("Read", new ReadOnlySpan<byte>(buffer, offset, read));
79+
if (count > 0)
80+
{
81+
Log("Read", new ReadOnlySpan<byte>(buffer, offset, read));
82+
}
8083
return read;
8184
}
8285

8386
public override int Read(Span<byte> destination)
8487
{
8588
int read = _inner.Read(destination);
86-
Log("Read", destination.Slice(0, read));
89+
if (!destination.IsEmpty)
90+
{
91+
Log("Read", destination.Slice(0, read));
92+
}
8793
return read;
8894
}
8995

9096
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
9197
{
9298
int read = await _inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
93-
Log("ReadAsync", new ReadOnlySpan<byte>(buffer, offset, read));
99+
if (count > 0)
100+
{
101+
Log("ReadAsync", new ReadOnlySpan<byte>(buffer, offset, read));
102+
}
94103
return read;
95104
}
96105

97106
public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
98107
{
99108
int read = await _inner.ReadAsync(destination, cancellationToken);
100-
Log("ReadAsync", destination.Span.Slice(0, read));
109+
if (!destination.IsEmpty)
110+
{
111+
Log("ReadAsync", destination.Span.Slice(0, read));
112+
}
101113
return read;
102114
}
103115

src/Servers/Kestrel/Core/src/Middleware/LoggingDuplexPipe.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
88

99
internal sealed class LoggingDuplexPipe : DuplexPipeStreamAdapter<LoggingStream>
1010
{
11+
private static readonly StreamPipeReaderOptions _defaultReaderOptions = new(useZeroByteReads: true);
12+
private static readonly StreamPipeWriterOptions _defaultWriterOptions = new();
13+
1114
public LoggingDuplexPipe(IDuplexPipe transport, ILogger logger) :
12-
base(transport, stream => new LoggingStream(stream, logger))
15+
base(transport, _defaultReaderOptions, _defaultWriterOptions, stream => new LoggingStream(stream, logger))
1316
{
1417
}
1518
}

src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
2020
using Microsoft.AspNetCore.Testing;
2121
using Microsoft.Extensions.Logging;
22+
using Moq;
2223
using Xunit;
2324

2425
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
@@ -80,6 +81,36 @@ public async Task PipesAreNotPersistedAcrossRequests()
8081
}
8182
}
8283

84+
[Fact]
85+
public async Task RequestBodyPipeReaderDoesZeroByteReads()
86+
{
87+
await using (var server = new TestServer(async context =>
88+
{
89+
var bufferLengths = new List<int>();
90+
91+
var mockStream = new Mock<Stream>();
92+
93+
mockStream.Setup(s => s.CanRead).Returns(true);
94+
mockStream.Setup(s => s.ReadAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>())).Returns<Memory<byte>, CancellationToken>((buffer, token) =>
95+
{
96+
bufferLengths.Add(buffer.Length);
97+
return ValueTask.FromResult(0);
98+
});
99+
100+
context.Request.Body = mockStream.Object;
101+
var data = await context.Request.BodyReader.ReadAsync();
102+
103+
Assert.Equal(2, bufferLengths.Count);
104+
Assert.Equal(0, bufferLengths[0]);
105+
Assert.Equal(4096, bufferLengths[1]);
106+
107+
await context.Response.WriteAsync("hello, world");
108+
}, new TestServiceContext(LoggerFactory)))
109+
{
110+
Assert.Equal("hello, world", await server.HttpClientSlim.GetStringAsync($"http://localhost:{server.Port}/"));
111+
}
112+
}
113+
83114
[Fact]
84115
public async Task RequestBodyReadAsyncCanBeCancelled()
85116
{

0 commit comments

Comments
 (0)