Skip to content

Studio cannot open or download completed exports larger than 2 GiB #6601

Closed

Description

@AksharP5

Describe the bug

A finished MOV export larger than 2 GiB appears as complete in Studio's Renders panel, but opening or downloading it returns HTTP 500. The three render file routes call readFileSync() before constructing the response, and Node refuses a file this large with ERR_FS_FILE_TOO_LARGE.

This affects high-quality masters such as a few minutes of 1080p ProRes. The file itself is valid: FFprobe identifies the generated fixture as ProRes, and FFmpeg decodes its first and last frames.

Minimal reproduction

Run from a repository checkout after bun install --frozen-lockfile and bun run build. Everything below uses generated test footage. It needs Node and FFmpeg, with enough disk space for a 2.3 GB file.

mkdir -p /tmp/hf-large-export/renders
ffmpeg -hide_banner -loglevel error \
  -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=1 -an \
  -c:v prores_ks -profile:v 3 -pix_fmt yuv422p10le -threads 4 \
  /tmp/hf-large-export/seed.mov
ffmpeg -hide_banner -loglevel error -stream_loop -1 \
  -i /tmp/hf-large-export/seed.mov -map 0:v -c copy -fs 2300000000 \
  /tmp/hf-large-export/renders/large.mov
ffprobe -v error -show_entries format=size,duration:stream=codec_name,width,height \
  -of json /tmp/hf-large-export/renders/large.mov

Save the following as http-proof.mjs. It starts the real render routes on an ephemeral loopback port, discovers the existing export from disk, then hashes each complete HTTP response against the file. The adapter supplies only the project and render directory; it does not replace file serving.

Complete HTTP reproduction
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

const root = process.cwd();
const { Hono } = await import(pathToFileURL(join(root, 'packages/studio-server/node_modules/hono/dist/index.js')));
const { serve } = await import(pathToFileURL(join(root, 'packages/cli/node_modules/@hono/node-server/dist/index.mjs')));
const { registerRenderRoutes } = await import(pathToFileURL(join(root, 'packages/studio-server/src/routes/render.ts')));
const project = process.argv[2];
const app = new Hono();
const errors = [];
app.onError((error, c) => { errors.push({ code: error.code, message: error.message }); return c.text(error.code ?? error.message, 500); });
registerRenderRoutes(app, {
  resolveProject: id => ({ id, dir: project }),
  listProjects: () => [{ id: 'demo', dir: project }],
  bundle: async () => null,
  lint: async () => ({ findings: [] }),
  runtimeUrl: '/api/runtime.js',
  rendersDir: () => join(project, 'renders'),
  startRender: () => { throw new Error('Only existing exports are used'); },
});
const server = serve({ fetch: app.fetch, hostname: '127.0.0.1', port: 0 });
if (!server.listening) await new Promise(resolve => server.once('listening', resolve));
const base = `http://127.0.0.1:${server.address().port}`;
async function digest(readable) {
  const hash = createHash('sha256');
  let bytes = 0;
  for await (const chunk of readable) { bytes += chunk.length; hash.update(chunk); }
  return { bytes, sha256: hash.digest('hex') };
}
try {
  const disk = await digest(createReadStream(join(project, 'renders/large.mov')));
  console.log(JSON.stringify({ disk }));
  const listing = await (await fetch(`${base}/projects/demo/renders`)).json();
  console.log(JSON.stringify({ listing }));
  for (const route of ['/projects/demo/renders/file/large.mov', '/render/large/view', '/render/large/download']) {
    await (await fetch(`${base}/projects/demo/renders`)).json();
    const response = await fetch(base + route);
    const body = await digest(response.body);
    console.log(JSON.stringify({ route, status: response.status, length: response.headers.get('content-length'), type: response.headers.get('content-type'), disposition: response.headers.get('content-disposition'), body, matchesDisk: body.sha256 === disk.sha256 }));
  }
  console.log(JSON.stringify({ errors }));
} finally {
  server.closeAllConnections();
  await new Promise(resolve => server.close(resolve));
}

Steps to reproduce

node --import tsx http-proof.mjs /tmp/hf-large-export

Studio's Renders panel uses /api/projects/:id/renders/file/:filename for its thumbnail, open and download actions. The reproduction mounts the routes without the /api prefix and also exercises both job-specific delivery routes after discovering the saved export.

Expected behavior

The completed export opens/downloads successfully. Its HTTP body matches the file on disk.

Actual behavior

On canonical main 3b2058a2b (the render route is unchanged on refreshed main 439d68567):

Request Actual status Error
/projects/demo/renders/file/large.mov 500 ERR_FS_FILE_TOO_LARGE
/render/large/view 500 ERR_FS_FILE_TOO_LARGE
/render/large/download 500 ERR_FS_FILE_TOO_LARGE

All three log File size (2300130263) is greater than 2 GiB. The listing reports the same 2,300,130,263-byte file as complete. The synthetic fixture is 239.466667 seconds, 1920×1080 ProRes. Its SHA-256 in this run is a54520dfaaa0daa1fdb5f4c75821f8d927e6351ee5f414f3c1dd3e7d256667fa.

Environment

Linux x64; Node 24.22.1; Bun 1.4.4; HyperFrames main 0.8.209. This is an HTTP/filesystem reproduction and does not launch Chrome or render a composition.

Related work

#3745 streams composition preview assets, while this report concerns finished render files. #6033 concerns open-ended media ranges. #5578 adds directory/PNG-sequence exports and touches the same route, but its single-file response still buffers the complete export; this report does not propose changes to that directory workflow.

Activity

  1. added
    bugSomething isn't working
    triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.
    on Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingtriage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions