Skip to content

Commit 2fb5646

Browse files
committed
feat(headers): add decompression control and Accept-Ranges support
- Add DownloadHeaderResult type with shouldDecompressBody flag - Return shouldDecompressBody to let caller handle server-side decompression - Add Accept-Ranges header (bytes when size known, none otherwise) - Disable Accept-Ranges when decompressing (can't seek in stream) - Skip Content-Encoding for media types (video/audio) that need seeking - Fix null checks for metadata.size
1 parent a50daec commit 2fb5646

File tree

1 file changed

+42
-5
lines changed

1 file changed

+42
-5
lines changed

packages/utility/file-server/src/http/headers.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
8589
export 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

126163
export const handleS3DownloadResponseHeaders = (

0 commit comments

Comments
 (0)