@@ -61,7 +61,9 @@ type ScanState = {
6161 * (created lazily - responses that never hit the binary path don't need one) */
6262 decoder : TextDecoder | undefined ,
6363 zlibChunks : Array < Buffer > ,
64- /** length of the decompressed output already scanned, so each chunk only rescans the new tail */
64+ /** streaming decoder for decompressed deltas, which may end inside a multi-byte character */
65+ decompressedDecoder : TextDecoder | undefined ,
66+ /** byte length of the decompressed output already scanned, so each chunk only scans new bytes */
6567 decompressedLength : number ,
6668 flushTimer : ReturnType < typeof setTimeout > | undefined ,
6769} ;
@@ -72,6 +74,7 @@ function getScanState(res: any): ScanState {
7274 carry : '' ,
7375 decoder : undefined ,
7476 zlibChunks : [ ] ,
77+ decompressedDecoder : undefined ,
7578 decompressedLength : 0 ,
7679 flushTimer : undefined ,
7780 } satisfies ScanState ;
@@ -87,6 +90,13 @@ function decodeChunk(state: ScanState, chunk?: Uint8Array, isFinal?: boolean) {
8790 return state . decoder . decode ( chunk , { stream : ! isFinal } ) ;
8891}
8992
93+ function decodeDecompressedDelta ( state : ScanState , decompressed : Buffer , isFinal = false ) {
94+ state . decompressedDecoder ||= new TextDecoder ( ) ;
95+ const delta = decompressed . subarray ( state . decompressedLength ) ;
96+ state . decompressedLength = decompressed . byteLength ;
97+ return state . decompressedDecoder . decode ( delta , { stream : ! isFinal } ) ;
98+ }
99+
90100function clearPendingFlush ( state : ScanState ) {
91101 if ( state . flushTimer !== undefined ) {
92102 clearTimeout ( state . flushTimer ) ;
@@ -201,6 +211,12 @@ export function patchGlobalServerResponse(opts?: {
201211 const state = getScanState ( this ) ;
202212 clearPendingFlush ( state ) ;
203213
214+ // A later chunk may be redacted to a different length. Once write() sends the
215+ // headers, Content-Length cannot be corrected, so use chunked framing instead.
216+ if ( opts ?. redactInsteadOfThrow && ! this . headersSent && this . getHeader ( 'content-length' ) !== undefined ) {
217+ this . removeHeader ( 'content-length' ) ;
218+ }
219+
204220 // have to deal with compressed data, which is awkward but possible
205221 const compressionType = this . getHeader ( 'Content-Encoding' ) ;
206222 let chunkStr ;
@@ -223,9 +239,7 @@ export function patchGlobalServerResponse(opts?: {
223239 // partial stream fails to decode here and gets scanned once more chunks arrive.
224240 try {
225241 const decompressedChunk = decompress ( Buffer . concat ( state . zlibChunks ) ) ;
226- const fullDecompressedData = decompressedChunk . toString ( 'utf-8' ) ;
227- chunkStr = fullDecompressedData . substring ( state . decompressedLength ) ;
228- state . decompressedLength = fullDecompressedData . length ;
242+ chunkStr = decodeDecompressedDelta ( state , decompressedChunk ) ;
229243 } catch ( err ) {
230244 // partial compressed data that doesn't decode yet — scanned when more chunks arrive
231245 }
@@ -296,18 +310,18 @@ export function patchGlobalServerResponse(opts?: {
296310
297311 if ( isBinaryChunk && compressionType ) {
298312 const decompress = getDecompressor ( String ( compressionType ) . toLowerCase ( ) ) ;
299- let decompressed : string | undefined ;
313+ let decompressed : Buffer | undefined ;
300314 if ( decompress ) {
301315 state . zlibChunks . push ( endChunk as Buffer ) ;
302316 try {
303- decompressed = decompress ( Buffer . concat ( state . zlibChunks ) ) . toString ( 'utf-8' ) ;
317+ decompressed = decompress ( Buffer . concat ( state . zlibChunks ) ) ;
304318 } catch ( err ) {
305319 // stream didn't decode, nothing more we can do at this point
306320 }
307321 }
308322 if ( decompressed !== undefined ) {
309323 // compressed output can't be scrubbed, so a detected leak always throws (see write above)
310- scanForLeaks ( state . carry + decompressed . substring ( state . decompressedLength ) , {
324+ scanForLeaks ( state . carry + decodeDecompressedDelta ( state , decompressed , true ) , {
311325 method : 'patched ServerResponse.end' ,
312326 file : ( this as any ) . req ?. url ,
313327 } ) ;
0 commit comments