@@ -284,6 +284,107 @@ public async Task SetHttpOutputStreamAsync_NullPrelude_WritesNoPreludeBytes()
284284 Assert . Empty ( output . ToArray ( ) ) ;
285285 }
286286
287+ // ---- Prelude + delimiter single-chunk tests (via ChunkedStreamWriter) ----
288+
289+ [ Fact ]
290+ public async Task SetHttpOutputStreamAsync_WithPrelude_ViaChunkedWriter_ProducesSingleChunk ( )
291+ {
292+ var preludeJson = Encoding . UTF8 . GetBytes ( "{\" statusCode\" :200}" ) ;
293+ var rs = new ResponseStream ( preludeJson ) ;
294+ var rawOutput = new MemoryStream ( ) ;
295+ var chunkedWriter = new ChunkedStreamWriter ( rawOutput ) ;
296+
297+ await rs . SetHttpOutputStreamAsync ( chunkedWriter ) ;
298+
299+ var wireBytes = Encoding . ASCII . GetString ( rawOutput . ToArray ( ) ) ;
300+
301+ // The prelude (18 bytes) + delimiter (8 bytes) = 26 bytes = 0x1A
302+ // Should be exactly one chunk: "1A\r\n{prelude}{8 null bytes}\r\n"
303+ var expectedDataLength = preludeJson . Length + 8 ; // 26
304+ var expectedHex = expectedDataLength . ToString ( "X" ) ;
305+ Assert . StartsWith ( $ "{ expectedHex } \r \n ", wireBytes ) ;
306+
307+ // Verify there is only one chunk header (only one hex size prefix)
308+ var chunkCount = 0 ;
309+ var remaining = wireBytes ;
310+ while ( remaining . Length > 0 )
311+ {
312+ var crlfIndex = remaining . IndexOf ( "\r \n " , StringComparison . Ordinal ) ;
313+ if ( crlfIndex < 0 ) break ;
314+ var sizeStr = remaining . Substring ( 0 , crlfIndex ) ;
315+ if ( int . TryParse ( sizeStr , System . Globalization . NumberStyles . HexNumber , null , out var chunkSize ) && chunkSize >= 0 )
316+ {
317+ chunkCount ++ ;
318+ // Skip past: hex\r\n{data}\r\n
319+ remaining = remaining . Substring ( crlfIndex + 2 + chunkSize + 2 ) ;
320+ }
321+ else
322+ {
323+ break ;
324+ }
325+ }
326+ Assert . Equal ( 1 , chunkCount ) ;
327+ }
328+
329+ [ Fact ]
330+ public async Task SetHttpOutputStreamAsync_WithPrelude_ViaChunkedWriter_DelimiterImmediatelyFollowsPrelude ( )
331+ {
332+ var preludeJson = Encoding . UTF8 . GetBytes ( "{\" statusCode\" :201}" ) ;
333+ var rs = new ResponseStream ( preludeJson ) ;
334+ var rawOutput = new MemoryStream ( ) ;
335+ var chunkedWriter = new ChunkedStreamWriter ( rawOutput ) ;
336+
337+ await rs . SetHttpOutputStreamAsync ( chunkedWriter ) ;
338+
339+ // Parse the chunk to get the raw data payload
340+ var wireBytes = rawOutput . ToArray ( ) ;
341+ var wireStr = Encoding . ASCII . GetString ( wireBytes ) ;
342+ var firstCrlf = wireStr . IndexOf ( "\r \n " , StringComparison . Ordinal ) ;
343+ var dataStart = firstCrlf + 2 ;
344+ var dataLength = preludeJson . Length + 8 ;
345+ var chunkData = new byte [ dataLength ] ;
346+ Array . Copy ( wireBytes , dataStart , chunkData , 0 , dataLength ) ;
347+
348+ // First part should be the prelude JSON
349+ Assert . Equal ( preludeJson , chunkData [ ..preludeJson . Length ] ) ;
350+ // Immediately followed by 8 null bytes (delimiter)
351+ Assert . Equal ( new byte [ 8 ] , chunkData [ preludeJson . Length ..] ) ;
352+ }
353+
354+ [ Fact ]
355+ public async Task SetHttpOutputStreamAsync_WithPrelude_ViaChunkedWriter_HandlerDataInSeparateChunk ( )
356+ {
357+ var preludeJson = Encoding . UTF8 . GetBytes ( "{\" statusCode\" :200}" ) ;
358+ var rs = new ResponseStream ( preludeJson ) ;
359+ var rawOutput = new MemoryStream ( ) ;
360+ var chunkedWriter = new ChunkedStreamWriter ( rawOutput ) ;
361+
362+ await rs . SetHttpOutputStreamAsync ( chunkedWriter ) ;
363+ await rs . WriteAsync ( Encoding . UTF8 . GetBytes ( "body data" ) , 0 , 9 ) ;
364+
365+ var wireStr = Encoding . ASCII . GetString ( rawOutput . ToArray ( ) ) ;
366+
367+ // Should have exactly 2 chunks: one for prelude+delimiter, one for body
368+ var chunkCount = 0 ;
369+ var remaining = wireStr ;
370+ while ( remaining . Length > 0 )
371+ {
372+ var crlfIndex = remaining . IndexOf ( "\r \n " , StringComparison . Ordinal ) ;
373+ if ( crlfIndex < 0 ) break ;
374+ var sizeStr = remaining . Substring ( 0 , crlfIndex ) ;
375+ if ( int . TryParse ( sizeStr , System . Globalization . NumberStyles . HexNumber , null , out var chunkSize ) && chunkSize >= 0 )
376+ {
377+ chunkCount ++ ;
378+ remaining = remaining . Substring ( crlfIndex + 2 + chunkSize + 2 ) ;
379+ }
380+ else
381+ {
382+ break ;
383+ }
384+ }
385+ Assert . Equal ( 2 , chunkCount ) ;
386+ }
387+
287388 // ---- MarkCompleted idempotency ----
288389
289390 [ Fact ]
0 commit comments