33 *--------------------------------------------------------------------------------------------*/
44
55using System . Reflection ;
6+ using System . Text ;
67using System . Text . Json ;
78using System . Text . Json . Serialization . Metadata ;
89using Xunit ;
@@ -93,6 +94,57 @@ public async Task JsonRpc_Cancels_And_Disposes_Pending_Requests()
9394 await Assert . ThrowsAnyAsync < ObjectDisposedException > ( ( ) => pending ) ;
9495 }
9596
97+ [ Fact ]
98+ public async Task JsonRpc_Does_Not_Retain_Oversized_Receive_Buffer ( )
99+ {
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 ) ;
106+ using var rpc = new JsonRpcReflection ( Stream . Null , receiveStream ) ;
107+
108+ var carriedResponse = rpc . InvokeAsync < string > ( "pending" , args : null ) ;
109+ rpc . StartListening ( ) ;
110+
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 (
119+ receiveStream . PostFrameReadBufferSize ,
120+ Task . Delay ( TimeSpan . FromSeconds ( 5 ) ) ) ;
121+ Assert . Same ( receiveStream . PostFrameReadBufferSize , readCompleted ) ;
122+ Assert . InRange ( await receiveStream . PostFrameReadBufferSize , 1 , 1024 * 1024 ) ;
123+ }
124+
125+ private static byte [ ] CreateResponseFrame ( long id , string result , int headerPaddingLength = 0 )
126+ {
127+ using var bodyStream = new MemoryStream ( ) ;
128+ using ( var writer = new Utf8JsonWriter ( bodyStream ) )
129+ {
130+ writer . WriteStartObject ( ) ;
131+ writer . WriteString ( "jsonrpc" , "2.0" ) ;
132+ writer . WriteNumber ( "id" , id ) ;
133+ writer . WriteString ( "result" , result ) ;
134+ writer . WriteEndObject ( ) ;
135+ }
136+
137+ var body = bodyStream . ToArray ( ) ;
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 ") ;
142+ var frame = new byte [ header . Length + body . Length ] ;
143+ header . CopyTo ( frame , 0 ) ;
144+ body . CopyTo ( frame , header . Length ) ;
145+ return frame ;
146+ }
147+
96148 private static int GetRemoteErrorCode ( Exception exception )
97149 {
98150 var property = exception . GetType ( ) . GetProperty ( "ErrorCode" , BindingFlags . Instance | BindingFlags . Public ) ;
@@ -170,12 +222,17 @@ private sealed class JsonRpcReflection : IDisposable
170222 private readonly object _instance ;
171223
172224 public JsonRpcReflection ( Stream stream )
225+ : this ( stream , stream )
226+ {
227+ }
228+
229+ public JsonRpcReflection ( Stream sendStream , Stream receiveStream )
173230 {
174231 _instance = Activator . CreateInstance (
175232 JsonRpcType ,
176233 BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic ,
177234 binder : null ,
178- args : [ stream , stream , SerializerOptions , null ] ,
235+ args : [ sendStream , receiveStream , SerializerOptions , null ] ,
179236 culture : null ) ! ;
180237 }
181238
@@ -198,6 +255,82 @@ public async Task<T> InvokeAsync<T>(string methodName, object?[]? args, Cancella
198255 public void Dispose ( ) => ( ( IDisposable ) _instance ) . Dispose ( ) ;
199256 }
200257
258+ private sealed class CoalescedFramesThenWaitStream : Stream
259+ {
260+ private readonly TaskCompletionSource < int > _postFrameReadBufferSize =
261+ new ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
262+ private readonly byte [ ] _frames ;
263+ private readonly int _firstFrameLength ;
264+ private int _offset ;
265+
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+
276+ public Task < int > PostFrameReadBufferSize => _postFrameReadBufferSize . Task ;
277+
278+ public override bool CanRead => true ;
279+
280+ public override bool CanSeek => false ;
281+
282+ public override bool CanWrite => false ;
283+
284+ public override long Length => throw new NotSupportedException ( ) ;
285+
286+ public override long Position { get => throw new NotSupportedException ( ) ; set => throw new NotSupportedException ( ) ; }
287+
288+ public override int Read ( byte [ ] buffer , int offset , int count ) => throw new NotSupportedException ( ) ;
289+
290+ public override Task < int > ReadAsync ( byte [ ] buffer , int offset , int count , CancellationToken cancellationToken ) =>
291+ ReadCoreAsync ( buffer . AsMemory ( offset , count ) , cancellationToken ) . AsTask ( ) ;
292+
293+ #if NET8_0_OR_GREATER
294+ public override
295+ #else
296+ internal
297+ #endif
298+ ValueTask < int > ReadAsync ( Memory < byte > destination , CancellationToken cancellationToken = default ) =>
299+ ReadCoreAsync ( destination , cancellationToken ) ;
300+
301+ public override void Flush ( )
302+ {
303+ }
304+
305+ public override long Seek ( long offset , SeekOrigin origin ) => throw new NotSupportedException ( ) ;
306+
307+ public override void SetLength ( long value ) => throw new NotSupportedException ( ) ;
308+
309+ public override void Write ( byte [ ] buffer , int offset , int count ) => throw new NotSupportedException ( ) ;
310+
311+ private ValueTask < int > ReadCoreAsync ( Memory < byte > destination , CancellationToken cancellationToken )
312+ {
313+ if ( _offset >= _frames . Length )
314+ {
315+ _postFrameReadBufferSize . TrySetResult ( destination . Length ) ;
316+ return new ValueTask < int > ( WaitForCancellationAsync ( cancellationToken ) ) ;
317+ }
318+
319+ var startingOffset = _offset ;
320+ var bytesRead = Math . Min ( destination . Length , _frames . Length - _offset ) ;
321+ _frames . AsMemory ( _offset , bytesRead ) . CopyTo ( destination ) ;
322+ _offset += 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 ;
331+ }
332+ }
333+
201334 private sealed class InMemoryDuplexStream : Stream
202335 {
203336 private readonly Queue < byte > _buffer = new ( ) ;
0 commit comments