@@ -82,14 +82,19 @@ const buildDisposition = (req: Request, metadata: DownloadMetadata, filename: st
8282 return `${ type } ; filename="${ fallbackName } "; filename*=UTF-8''${ encoded } `
8383}
8484
85+ export type DownloadHeaderResult = {
86+ shouldDecompressBody : boolean
87+ }
88+
8589export const handleDownloadResponseHeaders = (
8690 req : Request ,
8791 res : Response ,
8892 metadata : DownloadMetadata ,
8993 { byteRange = undefined , rawMode = false } : DownloadOptions ,
90- ) => {
94+ ) : DownloadHeaderResult => {
9195 const baseName = metadata . name || 'download'
9296 const fileName = metadata . type === 'file' ? baseName : `${ baseName } .zip`
97+ let shouldDecompressBody = false
9398
9499 if ( metadata . type === 'file' ) {
95100 const contentType =
@@ -99,20 +104,50 @@ export const handleDownloadResponseHeaders = (
99104 const compressedButNotEncrypted = metadata . isCompressed && ! metadata . isEncrypted
100105
101106 // Only set Content-Encoding for document navigations where browsers auto-decompress
107+ // Don't set it for <img>, <video>, fetch(), etc. as browsers won't auto-decompress those
102108 const shouldHandleEncoding = req . query . ignoreEncoding
103109 ? req . query . ignoreEncoding !== 'true'
104110 : isDocumentNavigation ( req )
105111
106- if ( compressedButNotEncrypted && shouldHandleEncoding && ! rawMode && ! byteRange ) {
112+ const mimeType = contentType . toLowerCase ( )
113+ const isMediaType = mimeType . startsWith ( 'video/' ) || mimeType . startsWith ( 'audio/' )
114+
115+ const mustDecompress =
116+ compressedButNotEncrypted &&
117+ ( ! shouldHandleEncoding || rawMode || byteRange != null || isMediaType )
118+ // Always advertise range support if we have size, even for compressed media
119+ // because we'll decompress them on-the-fly to support seeking
120+ const canAdvertiseRanges = metadata . size != null
121+ if ( canAdvertiseRanges ) {
122+ res . set ( 'Accept-Ranges' , 'bytes' )
123+ } else {
124+ res . set ( 'Accept-Ranges' , 'none' )
125+ }
126+
127+ if (
128+ compressedButNotEncrypted &&
129+ shouldHandleEncoding &&
130+ ! rawMode &&
131+ ! byteRange &&
132+ ! isMediaType
133+ ) {
107134 res . set ( 'Content-Encoding' , 'deflate' )
135+ } else if ( mustDecompress ) {
136+ shouldDecompressBody = true
108137 }
109138
110- if ( byteRange ) {
139+ if ( mustDecompress ) {
140+ // When decompressing, we can't know the output size upfront
141+ // Don't set Content-Length - this will use chunked transfer encoding
142+ // Also don't advertise ranges since we can't seek in decompressed stream
143+ res . set ( 'Accept-Ranges' , 'none' )
144+ } else if ( byteRange && metadata . size != null ) {
145+ // For range requests on non-compressed content
111146 res . status ( 206 )
112- res . set ( 'Content-Range' , `bytes ${ byteRange [ 0 ] } -${ byteRange [ 1 ] } /${ metadata . size } ` )
113147 const upperBound = byteRange [ 1 ] ?? Number ( metadata . size ) - 1
148+ res . set ( 'Content-Range' , `bytes ${ byteRange [ 0 ] } -${ upperBound } /${ metadata . size } ` )
114149 res . set ( 'Content-Length' , ( upperBound - byteRange [ 0 ] + 1 ) . toString ( ) )
115- } else if ( metadata . size ) {
150+ } else if ( metadata . size != null ) {
116151 res . set ( 'Content-Length' , metadata . size . toString ( ) )
117152 }
118153 } else {
@@ -121,6 +156,8 @@ export const handleDownloadResponseHeaders = (
121156 }
122157
123158 res . set ( 'Content-Disposition' , buildDisposition ( req , metadata , fileName ) )
159+
160+ return { shouldDecompressBody }
124161}
125162
126163export const handleS3DownloadResponseHeaders = (
0 commit comments