@@ -3,7 +3,7 @@ import { requestRaw } from '@/lib/api/client/request'
33import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders'
44import { exportWorkspaceFileSnapshotContract } from '@/lib/api/contracts/workspace-files'
55import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
6- import { isMarkdownFile } from '@/lib/uploads/utils/file'
6+ import { isMarkdownFile , needsRenderedArtifact } from '@/lib/uploads/utils/file'
77import { normalizeMimeType } from '@/lib/uploads/utils/mime'
88
99/** Action-time content from the mounted viewer, scoped so another file cannot consume it. */
@@ -14,11 +14,11 @@ export interface FileDownloadSource {
1414}
1515
1616/**
17- * Above this a file is handed to the browser's download manager instead of being read into the
18- * page. Holding it as a `Blob` costs its whole size in tab memory and shows no progress until the
19- * last byte lands; the download manager streams it to disk. Below it, fetching first keeps a
20- * rejection — a document still compiling — on screen as an error rather than as a failed entry
21- * in the browser's downloads list.
17+ * Above this a stored file is handed to the browser's download manager instead of being read into
18+ * the page. Holding it as a `Blob` costs its whole size in tab memory and shows no progress until
19+ * the last byte lands; the download manager streams it to disk. Below it, and for a generated
20+ * document, which must compile first, fetching keeps a rejection on screen as an error rather than
21+ * as a failed entry in the browser's downloads list.
2222 */
2323const IN_PAGE_DOWNLOAD_MAX_BYTES = 100 * 1024 * 1024
2424
@@ -97,9 +97,16 @@ export async function triggerFileDownload(
9797 ? `/api/files/export/${ encodeURIComponent ( record . id ) } `
9898 : `/api/files/serve/${ encodeURIComponent ( record . key ) } ?context=${ record . storageContext ?? 'workspace' } &t=${ Date . now ( ) } `
9999
100- if ( ! exportsMarkdown && record . size > IN_PAGE_DOWNLOAD_MAX_BYTES ) {
101- // boundary-raw-fetch: HEAD probe of a binary path with no contract, so a refusal still surfaces here
102- const probe = await fetch ( url , { method : 'HEAD' , cache : 'no-store' } )
100+ if (
101+ ! exportsMarkdown &&
102+ ! needsRenderedArtifact ( record . type , record . name ) &&
103+ record . size > IN_PAGE_DOWNLOAD_MAX_BYTES
104+ ) {
105+ // A first-byte read opens storage itself, so a missing object is refused here, in place,
106+ // rather than as a failed entry in the browser's downloads list.
107+ // boundary-raw-fetch: ranged probe of a binary path with no contract
108+ const probe = await fetch ( url , { headers : { Range : 'bytes=0-0' } , cache : 'no-store' } )
109+ await probe . body ?. cancel ( )
103110 if ( ! probe . ok ) throw new Error ( `Failed to download "${ record . name } "` )
104111 clickDownloadLink ( url , record . name )
105112 return
0 commit comments