@@ -147,12 +147,15 @@ describe("readResponseText", () => {
147147 expect ( releaseLock ) . toHaveBeenCalledTimes ( 1 ) ;
148148 } ) ;
149149
150- it ( "refuses to call unbounded .text() when maxBytes is set" , async ( ) => {
151- // Foreign Response without a body stream or arrayBuffer — the only way
152- // to read is .text() which buffers the full body before any check.
153- // When maxBytes is set the caller expects bounded memory, so we must
154- // fail-closed rather than call .text() unbounded.
155- const longText = "x" . repeat ( 10_000 ) ;
150+ it ( "truncates text-only fallback at byte boundaries when maxBytes is set" , async ( ) => {
151+ // Foreign Response without a body stream or arrayBuffer — only .text()
152+ // is available. Truncation must be byte-accurate so multi-byte UTF-8
153+ // content is not corrupted (naive text.slice would cut mid-codepoint).
154+ const prefix = "x" . repeat ( 50 ) + "中文" ;
155+ const longText = prefix + "🔥" + "y" . repeat ( 50 ) ;
156+ const fullBytes = new TextEncoder ( ) . encode ( longText ) ;
157+ const CAP = new TextEncoder ( ) . encode ( prefix ) . byteLength ; // clean codepoint boundary
158+ expect ( fullBytes . byteLength ) . toBeGreaterThan ( CAP ) ;
156159 const response = {
157160 body : null as unknown ,
158161 headers : new Headers ( ) ,
@@ -161,13 +164,15 @@ describe("readResponseText", () => {
161164 } ,
162165 } as unknown as Response ;
163166
164- // With maxBytes → rejected (no bounded byte source available)
165- const capped = await readResponseText ( response , { maxBytes : 100 } ) ;
167+ // With maxBytes → byte-level truncation
168+ const capped = await readResponseText ( response , { maxBytes : CAP } ) ;
166169 expect ( capped . truncated ) . toBe ( true ) ;
167- expect ( capped . bytesRead ) . toBe ( 0 ) ;
168- expect ( capped . text ) . toBe ( "" ) ;
170+ expect ( capped . bytesRead ) . toBe ( CAP ) ;
171+ const expected = new TextDecoder ( ) . decode ( fullBytes . slice ( 0 , CAP ) ) ;
172+ expect ( capped . text ) . toBe ( expected ) ;
173+ expect ( capped . text ) . not . toContain ( "�" ) ;
169174
170- // Without maxBytes → .text() is safe (caller accepts full allocation )
175+ // Without maxBytes → passes through (regression )
171176 const noCapResponse = {
172177 body : null as unknown ,
173178 headers : new Headers ( ) ,
@@ -178,6 +183,6 @@ describe("readResponseText", () => {
178183 const full = await readResponseText ( noCapResponse ) ;
179184 expect ( full . text ) . toBe ( longText ) ;
180185 expect ( full . truncated ) . toBe ( false ) ;
181- expect ( full . bytesRead ) . toBe ( new TextEncoder ( ) . encode ( longText ) . byteLength ) ;
186+ expect ( full . bytesRead ) . toBe ( fullBytes . byteLength ) ;
182187 } ) ;
183188} ) ;
0 commit comments