@@ -295,9 +295,10 @@ describe('executeWebFetch — size + truncation', () => {
// 1.5M U+1F600 emojis: each is length 2 in UTF-16 (surrogate pair) and
// 4 bytes in UTF-8. body.length = 3,000,000 chars (~2.86 MiB by
// UTF-16 count) but Buffer.byteLength = 6,000,000 bytes (>5 MiB).
// Pre-fix the char-count comparison let this through; the byte-count
// check now rejects. No Content-Length header so the pre-flight
// guard doesn't fire — we're testing the POST-consumption check.
// v1.11.10: streaming reader catches this as body_too_large (was
// response_too_large in the post-consumption check). No
// Content-Length header so the pre-flight pass and the streaming
// path is the one that rejects.
const heavy = '😀' . repeat ( 1 _500_000 ) ;
const fakeFetch = vi . fn ( ) . mockResolvedValue (
new Response ( heavy , { status : 200 , headers : { 'content-type' : 'text/plain' } } ) ,
@@ -308,9 +309,8 @@ describe('executeWebFetch — size + truncation', () => {
) ;
expect ( 'error' in result ) . toBe ( true ) ;
if ( 'error' in result ) {
expect ( result . error ) . toBe ( 'response _too_large' ) ;
// Error reason should reference bytes, not character count.
expect ( result . reason ) . toMatch ( /bytes/ ) ;
expect ( result . error ) . toBe ( 'body _too_large' ) ;
expect ( result . reason ) . toMatch ( /exceeded/ ) ;
}
} ) ;
@@ -453,3 +453,138 @@ describe('executeWebFetch — redirect handling', () => {
expect ( fakeFetch . mock . calls [ 1 ] ! [ 0 ] ) . toBe ( 'https://example.com/foo' ) ;
} ) ;
} ) ;
// ============================================================================
// v1.11.10: streaming body cap — abort the response stream at MAX_BYTES
// ============================================================================
// MAX_BYTES is 5 * 1024 * 1024 = 5_242_880. Repeating this here (rather
// than importing) so a change to the cap surfaces as a test failure —
// the limit is part of the public contract.
const MAX_BYTES_TEST = 5 * 1024 * 1024 ;
// Build a Response whose body is a real ReadableStream. Uses pull() (not
// start()) so chunks are produced lazily — without backpressure, an
// unbounded start() enqueues everything and calls controller.close()
// before the consumer reads, which means a subsequent reader.cancel()
// finds the stream already closed and the cancel callback never fires.
// `cancelFlag` lets the test observe whether reader.cancel() reached the
// underlying source mid-stream.
function streamedResponse (
chunks : Uint8Array [ ] ,
init : { contentType? : string ; contentLength? : number | null ; cancelFlag ? : { cancelled : boolean } } = { } ,
) : Response {
let idx = 0 ;
const stream = new ReadableStream ( {
pull ( controller ) {
if ( idx >= chunks . length ) {
controller . close ( ) ;
return ;
}
controller . enqueue ( chunks [ idx ] ! ) ;
idx += 1 ;
} ,
cancel() {
if ( init . cancelFlag ) init . cancelFlag . cancelled = true ;
} ,
} ) ;
const headers : Record < string , string > = { } ;
if ( init . contentType ) headers [ 'content-type' ] = init . contentType ;
if ( init . contentLength !== undefined && init . contentLength !== null ) {
headers [ 'content-length' ] = String ( init . contentLength ) ;
}
return new Response ( stream , { status : 200 , headers } ) ;
}
describe ( 'executeWebFetch — streaming body cap (v1.11.10)' , ( ) = > {
it ( 'aborts the stream when a server lies about Content-Length and emits over the cap' , async ( ) = > {
// Honest header would have failed the pre-flight check. The lie is
// the point: pre-flight passes (100 < 5MB) and the streaming reader
// has to be the thing that catches the oversized body.
//
// Chunk count is deliberately higher than what the reader will
// consume (10 × 1MB available, but the reader will cancel after ~6
// chunks land it over 5MB). That headroom keeps the stream in
// 'readable' state at the moment reader.cancel() runs — otherwise
// a pull-then-close race could make the source close the stream
// before cancel reaches it, and the cancel() callback wouldn't fire.
const oneMB = new Uint8Array ( 1024 * 1024 ) . fill ( 65 ) ; // 'A'
const tenMBInChunks = Array . from ( { length : 10 } , ( ) = > oneMB ) ;
const cancelFlag = { cancelled : false } ;
const fakeFetch = vi . fn ( ) . mockResolvedValue (
streamedResponse ( tenMBInChunks , {
contentType : 'text/plain' ,
contentLength : 100 ,
cancelFlag ,
} ) ,
) ;
const result = await executeWebFetch (
{ url : 'https://example.com/lying-server' } ,
fakeFetch as unknown as typeof fetch ,
) ;
expect ( 'error' in result ) . toBe ( true ) ;
if ( 'error' in result ) {
expect ( result . error ) . toBe ( 'body_too_large' ) ;
expect ( result . reason ) . toMatch ( /exceeded/ ) ;
}
// Critical: reader.cancel() actually fired so the underlying
// connection / stream got released. Otherwise the abort would be
// notional and the server could keep streaming.
expect ( cancelFlag . cancelled ) . toBe ( true ) ;
} ) ;
it ( 'catches an oversized stream when Content-Length is omitted entirely' , async ( ) = > {
// Many real servers (chunked transfer-encoding, dynamic responses)
// never send Content-Length. The pre-flight check has nothing to
// gate on; the streaming reader is the only line of defense.
// 10 chunks vs the ~6 the reader will consume — same headroom
// rationale as the lying-Content-Length test above.
const oneMB = new Uint8Array ( 1024 * 1024 ) . fill ( 66 ) ; // 'B'
const tenMBInChunks = Array . from ( { length : 10 } , ( ) = > oneMB ) ;
const fakeFetch = vi . fn ( ) . mockResolvedValue (
streamedResponse ( tenMBInChunks , { contentType : 'text/plain' } ) ,
) ;
const result = await executeWebFetch (
{ url : 'https://example.com/no-length' } ,
fakeFetch as unknown as typeof fetch ,
) ;
expect ( 'error' in result && result . error ) . toBe ( 'body_too_large' ) ;
} ) ;
it ( 'passes a multi-chunk body that totals just under the cap' , async ( ) = > {
// Boundary case: MAX_BYTES - 1 bytes split across N chunks. The
// streaming reader's `total > maxBytes` check is strict-greater so
// exactly MAX_BYTES would still succeed; MAX_BYTES + 1 would fail.
// - 1 leaves clear headroom without coinciding with the boundary.
const targetTotal = MAX_BYTES_TEST - 1 ;
const chunkSize = 256 * 1024 ; // 256 KiB chunks
const chunks : Uint8Array [ ] = [ ] ;
let remaining = targetTotal ;
while ( remaining > 0 ) {
const size = Math . min ( chunkSize , remaining ) ;
chunks . push ( new Uint8Array ( size ) . fill ( 67 ) ) ; // 'C'
remaining -= size ;
}
const fakeFetch = vi . fn ( ) . mockResolvedValue (
streamedResponse ( chunks , { contentType : 'text/plain' } ) ,
) ;
const result = await executeWebFetch (
{ url : 'https://example.com/right-at-cap' } ,
fakeFetch as unknown as typeof fetch ,
) ;
// The streaming reader succeeded — we got a content shape, not an
// error. (Downstream truncate() will clamp the final string to
// MAX_CHARS_CAP=32000 and set truncated:true; that's the existing
// truncation logic and is exercised by its own test. The point of
// THIS test is that readBodyCapped didn't trip on a body that
// sits just under its byte limit.)
expect ( 'content' in result ) . toBe ( true ) ;
if ( 'content' in result ) {
expect ( result . content . length ) . toBeGreaterThan ( 0 ) ;
// All ASCII 'C's, so the leading 200 chars before any truncation
// marker should be all C — proves we read real bytes through the
// streaming reader rather than getting an empty buffer.
expect ( result . content . slice ( 0 , 200 ) ) . toBe ( 'C' . repeat ( 200 ) ) ;
}
} ) ;
} ) ;