@@ -271,6 +271,42 @@ public async Task StandaloneCrNotFollowedByLf_IsIncludedInLine()
271271 Assert . IsType < JsonRpcRequest > ( message ) ;
272272 }
273273
274+ [ Fact ]
275+ public async Task MultiByteSequenceInterruptedByNewline_BothLinesSkipped_NextValidLineDelivered ( )
276+ {
277+ // '€' encodes as 3 UTF-8 bytes: 0xE2 0x82 0xAC. If a newline is injected after the
278+ // first byte, the two resulting lines both contain invalid byte sequences:
279+ // Line 1: ...0xE2\n — a truncated 3-byte lead byte; invalid JSON in both old and new impl
280+ // Line 2: 0x82 0xAC...\n — continuation bytes without a lead byte; also invalid JSON
281+ //
282+ // Both the old StreamReader-based path (which produced U+FFFD replacement chars before
283+ // passing to JsonSerializer) and the new PipeReader-based path (which passes raw bytes to
284+ // JsonSerializer) raise JsonException for each line and silently skip them. A subsequent
285+ // valid JSON line must still be delivered.
286+ var ct = TestContext . Current . CancellationToken ;
287+ var pipe = new Pipe ( ) ;
288+ await using var transport = new StreamServerTransport ( pipe . Reader . AsStream ( ) , Stream . Null ) ;
289+
290+ // Build line 1: a JSON string where '€' is split after byte 0xE2, terminated with \n.
291+ byte [ ] euroBytes = Encoding . UTF8 . GetBytes ( "€" ) ; // [0xE2, 0x82, 0xAC]
292+ byte [ ] line1 = [ .. Encoding . UTF8 . GetBytes ( "{\" method\" :\" te" ) , euroBytes [ 0 ] , ( byte ) '\n ' ] ;
293+ // Build line 2: the remaining continuation bytes + rest of JSON, terminated with \n.
294+ byte [ ] line2 = [ euroBytes [ 1 ] , euroBytes [ 2 ] , .. Encoding . UTF8 . GetBytes ( "st\" }\n " ) ] ;
295+ // Build line 3: a valid JSON message that must survive the two bad lines.
296+ byte [ ] line3 = Encoding . UTF8 . GetBytes ( s_testJson + "\n " ) ;
297+
298+ byte [ ] allBytes = [ .. line1 , .. line2 , .. line3 ] ;
299+ await pipe . Writer . WriteAsync ( allBytes , ct ) ;
300+ await pipe . Writer . CompleteAsync ( ) ;
301+
302+ // Only the valid line 3 should produce a message; lines 1 and 2 are silently skipped.
303+ var message = await transport . MessageReader . ReadAsync ( ct ) ;
304+ Assert . IsType < JsonRpcRequest > ( message ) ;
305+
306+ // No further messages.
307+ Assert . False ( transport . MessageReader . TryRead ( out _ ) ) ;
308+ }
309+
274310 [ Fact ]
275311 public async Task LineWithNoTerminatingNewline_IsNotDelivered ( )
276312 {
0 commit comments