perf(studio): header-only nitro validation for fake/broken scan
CI / check (push) Successful in 4m23s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m23s

The cleanup scan used to read every .nitro bundle in full and decompress
the large PNG texture just to confirm the file is structurally valid. On
directories with hundreds of thousands of bundles this took minutes, the
reverse proxy cut the request at its 30s timeoutable with an HTML 504, and
the panel then crashed with "Unexpected token '<'".

Validate bundles with a cheap header-only read (a few KB, no decompression)
that mirrors parseNitroBundle's byte layout; only files whose header looks
suspicious get the expensive full parse. Robust against downloads that
landed as an HTML error page, truncated or zero-filled files. The scan
drops from minutes to seconds on large nitro directories.

Also guard the panel against non-JSON (proxy error page / HTML) responses
so it reports a clear error message instead of a JSON parse failure.
This commit is contained in:
openhands committed 2026-09-16 15:50:04 +02:00
1 parent 438277a17a
commit 3d7278e96d
2 files changed
+103 -3

No files matched your search

@@ -213,10 +213,24 @@ export function NitroCleanupPanel() {
setSelectedIcon(new Set());
try {
const res = await adminFetch("/api/admin/studio/nitro-cleanup");
const type = res.headers.get("content-type") ?? "";
if (!res.ok) {
const raw = await res.text();
throw new Error(
raw && !type.includes("application/json")
? `Scan failed (HTTP ${res.status}) — the server or reverse proxy returned a non-JSON response. This usually means a proxy/worker timeout; try again or raise the upstream timeout.`
: `Scan failed (HTTP ${res.status})`,
);
}
if (!type.includes("application/json")) {
throw new Error(
"Scan failed — the server returned a non-JSON response (likely an upstream timeout or proxy error page).",
);
}
const data = (await res.json()) as NitroCleanupScan & {
error?: string;
};
if (!res.ok) throw new Error(data.error || "Scan failed");
if (data.error) throw new Error(data.error);
setScan({
fake: data.fake ?? [],
broken: data.broken ?? [],