Skip to content

Commit ec5ff68

Browse files
adirh3Copilot
andcommitted
Strengthen oversized buffer carry-over test
Keep the receive stream open, coalesce a second valid response with the oversized frame, and verify both carried-message processing and the subsequent bounded read buffer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a251e16 commit ec5ff68

1 file changed

Lines changed: 53 additions & 18 deletions

File tree

dotnet/test/Unit/JsonRpcTests.cs

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,32 +97,48 @@ public async Task JsonRpc_Cancels_And_Disposes_Pending_Requests()
9797
[Fact]
9898
public async Task JsonRpc_Does_Not_Retain_Oversized_Receive_Buffer()
9999
{
100-
using var receiveStream = new FrameThenEofStream(CreateResponseFrame(2 * 1024 * 1024));
100+
var oversizedFrame = CreateResponseFrame(
101+
long.MaxValue,
102+
"ignored",
103+
headerPaddingLength: 1024 * 1024);
104+
var carriedFrame = CreateResponseFrame(1, "carried");
105+
using var receiveStream = new CoalescedFramesThenWaitStream(oversizedFrame, carriedFrame);
101106
using var rpc = new JsonRpcReflection(Stream.Null, receiveStream);
102107

108+
var carriedResponse = rpc.InvokeAsync<string>("pending", args: null);
103109
rpc.StartListening();
104110

105-
var completed = await Task.WhenAny(
111+
var responseCompleted = await Task.WhenAny(
112+
carriedResponse,
113+
Task.Delay(TimeSpan.FromSeconds(5)));
114+
Assert.Same(carriedResponse, responseCompleted);
115+
Assert.Equal("carried", await carriedResponse);
116+
Assert.True(receiveStream.FramesWereCoalesced);
117+
118+
var readCompleted = await Task.WhenAny(
106119
receiveStream.PostFrameReadBufferSize,
107120
Task.Delay(TimeSpan.FromSeconds(5)));
108-
Assert.Same(receiveStream.PostFrameReadBufferSize, completed);
121+
Assert.Same(receiveStream.PostFrameReadBufferSize, readCompleted);
109122
Assert.InRange(await receiveStream.PostFrameReadBufferSize, 1, 1024 * 1024);
110123
}
111124

112-
private static byte[] CreateResponseFrame(int payloadLength)
125+
private static byte[] CreateResponseFrame(long id, string result, int headerPaddingLength = 0)
113126
{
114127
using var bodyStream = new MemoryStream();
115128
using (var writer = new Utf8JsonWriter(bodyStream))
116129
{
117130
writer.WriteStartObject();
118131
writer.WriteString("jsonrpc", "2.0");
119-
writer.WriteNumber("id", long.MaxValue);
120-
writer.WriteString("result", new string('x', payloadLength));
132+
writer.WriteNumber("id", id);
133+
writer.WriteString("result", result);
121134
writer.WriteEndObject();
122135
}
123136

124137
var body = bodyStream.ToArray();
125-
var header = Encoding.ASCII.GetBytes($"Content-Length: {body.Length}\r\n\r\n");
138+
var paddingHeader = headerPaddingLength > 0
139+
? $"X-Padding: {new string('x', headerPaddingLength)}\r\n"
140+
: string.Empty;
141+
var header = Encoding.ASCII.GetBytes($"{paddingHeader}Content-Length: {body.Length}\r\n\r\n");
126142
var frame = new byte[header.Length + body.Length];
127143
header.CopyTo(frame, 0);
128144
body.CopyTo(frame, header.Length);
@@ -239,12 +255,24 @@ public async Task<T> InvokeAsync<T>(string methodName, object?[]? args, Cancella
239255
public void Dispose() => ((IDisposable)_instance).Dispose();
240256
}
241257

242-
private sealed class FrameThenEofStream(byte[] frame) : Stream
258+
private sealed class CoalescedFramesThenWaitStream : Stream
243259
{
244260
private readonly TaskCompletionSource<int> _postFrameReadBufferSize =
245261
new(TaskCreationOptions.RunContinuationsAsynchronously);
262+
private readonly byte[] _frames;
263+
private readonly int _firstFrameLength;
246264
private int _offset;
247265

266+
public CoalescedFramesThenWaitStream(byte[] firstFrame, byte[] secondFrame)
267+
{
268+
_firstFrameLength = firstFrame.Length;
269+
_frames = new byte[firstFrame.Length + secondFrame.Length];
270+
firstFrame.CopyTo(_frames, 0);
271+
secondFrame.CopyTo(_frames, firstFrame.Length);
272+
}
273+
274+
public bool FramesWereCoalesced { get; private set; }
275+
248276
public Task<int> PostFrameReadBufferSize => _postFrameReadBufferSize.Task;
249277

250278
public override bool CanRead => true;
@@ -257,19 +285,18 @@ private sealed class FrameThenEofStream(byte[] frame) : Stream
257285

258286
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
259287

260-
public override int Read(byte[] buffer, int offset, int count) =>
261-
ReadCore(buffer.AsMemory(offset, count));
288+
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
262289

263290
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
264-
Task.FromResult(ReadCore(buffer.AsMemory(offset, count)));
291+
ReadCoreAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
265292

266293
#if NET8_0_OR_GREATER
267294
public override
268295
#else
269296
internal
270297
#endif
271298
ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default) =>
272-
new(ReadCore(destination));
299+
ReadCoreAsync(destination, cancellationToken);
273300

274301
public override void Flush()
275302
{
@@ -281,18 +308,26 @@ public override void Flush()
281308

282309
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
283310

284-
private int ReadCore(Memory<byte> destination)
311+
private ValueTask<int> ReadCoreAsync(Memory<byte> destination, CancellationToken cancellationToken)
285312
{
286-
if (_offset >= frame.Length)
313+
if (_offset >= _frames.Length)
287314
{
288315
_postFrameReadBufferSize.TrySetResult(destination.Length);
289-
return 0;
316+
return new ValueTask<int>(WaitForCancellationAsync(cancellationToken));
290317
}
291318

292-
var bytesRead = Math.Min(destination.Length, frame.Length - _offset);
293-
frame.AsMemory(_offset, bytesRead).CopyTo(destination);
319+
var startingOffset = _offset;
320+
var bytesRead = Math.Min(destination.Length, _frames.Length - _offset);
321+
_frames.AsMemory(_offset, bytesRead).CopyTo(destination);
294322
_offset += bytesRead;
295-
return bytesRead;
323+
FramesWereCoalesced |= startingOffset < _firstFrameLength && _offset == _frames.Length;
324+
return new ValueTask<int>(bytesRead);
325+
}
326+
327+
private static async Task<int> WaitForCancellationAsync(CancellationToken cancellationToken)
328+
{
329+
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
330+
return 0;
296331
}
297332
}
298333

0 commit comments

Comments
 (0)