Expose actionable asset download and filesystem failure details
CI / check (push) Successful in 1m36s
CI / deploy (push) Successful in 1m16s
CI / e2e (push) Successful in 21s

This commit is contained in:
Simo committed 2026-09-05 14:38:10 +02:00
1 parent 8a919f85b2
commit 8c1efae296
4 files changed
+121 -18

No files matched your search

@@ -0,0 +1,76 @@
// @vitest-environment node
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const write = vi.hoisted(() => vi.fn());
vi.mock("node:fs", () => ({ promises: { writeFile: write } }));
import { downloadFile, tryDownloadCandidates } from "./download";
const fetchMock = vi.fn();
beforeEach(() => {
vi.stubGlobal("fetch", fetchMock);
fetchMock.mockReset();
write.mockReset().mockResolvedValue(undefined);
});
afterEach(() => vi.unstubAllGlobals());
const url = "https://cdn.example/73706/chair.swf";
describe("download failure diagnostics", () => {
it("reports HTTP status without writing an error page", async () => {
fetchMock.mockResolvedValue(new Response("missing", { status: 404 }));
expect(
(
await downloadFile(url, "/assets/chair.swf", {
validate: "swf",
maxRetries: 0,
})
).error,
).toBe("HTTP 404");
expect(write).not.toHaveBeenCalled();
});
it("distinguishes a successful download from denied file permissions", async () => {
fetchMock.mockResolvedValue(new Response(Buffer.from("FWS12345")));
write.mockRejectedValue(
Object.assign(new Error("denied"), { code: "EACCES" }),
);
expect(
(
await downloadFile(url, "/assets/chair.swf", {
validate: "swf",
maxRetries: 0,
})
).error,
).toBe("Cannot save file (EACCES)");
});
it("reports DNS failures", async () => {
fetchMock.mockRejectedValue(
Object.assign(new Error("fetch failed"), {
cause: { code: "ENOTFOUND" },
}),
);
expect(
(await downloadFile(url, "/assets/chair.swf", { maxRetries: 0 })).error,
).toBe("Network failure (ENOTFOUND)");
});
it("reports a non-SWF response", async () => {
fetchMock.mockResolvedValue(new Response("<html>not an asset</html>"));
expect(
(
await downloadFile(url, "/assets/chair.swf", {
validate: "swf",
maxRetries: 0,
})
).error,
).toBe("Response is not a SWF file");
});
it("forwards the failing path without URL credentials or query secrets", async () => {
fetchMock.mockResolvedValue(new Response("missing", { status: 404 }));
const failure = vi.fn();
await tryDownloadCandidates(
["https://user:[email protected]/0/chair.swf?token=private"],
"/assets/chair.swf",
"swf",
failure,
);
expect(failure).toHaveBeenCalledWith("cdn.example/0/chair.swf: HTTP 404");
});
});
+27 -8
View File
@@ -37,10 +37,15 @@ export async function tryDownloadCandidates(
urls: string[],
destPath: string,
kind: "swf" | "png",
onFailure?: (detail: string) => void,
): Promise<boolean> {
for (const url of urls) {
const res = await downloadFile(url, destPath, { validate: kind });
if (res.ok) return true;
const source = new URL(url);
onFailure?.(
`${source.host}${source.pathname}: ${res.error ?? "download failed"}`,
);
}
return false;
}
@@ -49,11 +54,12 @@ export async function downloadFile(
url: string,
destPath: string,
options?: { maxRetries?: number; validate?: "swf" | "png" | "nitro" },
): Promise<{ ok: boolean; size: number }> {
): Promise<{ ok: boolean; size: number; error?: string }> {
const maxRetries = options?.maxRetries ?? 3;
const baseDelay = 1000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
let stage: "download" | "write" = "download";
try {
if (attempt > 0) {
await new Promise((r) => setTimeout(r, baseDelay * 2 ** (attempt - 1)));
@@ -71,40 +77,53 @@ export async function downloadFile(
console.warn(
`[import-download] Download failed ${url}: ${res.status}${deterministic ? " (deterministic, not retrying)" : ` after ${maxRetries + 1} attempts`}`,
);
return { ok: false, size: 0 };
return { ok: false, size: 0, error: `HTTP ${res.status}` };
}
continue;
}
const buffer = Buffer.from(await res.arrayBuffer());
if (buffer.length < 8) {
if (attempt === maxRetries) return { ok: false, size: 0 };
if (attempt === maxRetries)
return { ok: false, size: 0, error: "Empty or truncated response" };
continue;
}
if (options?.validate === "swf" && !validateSwfBytes(buffer)) {
if (attempt === maxRetries) {
console.warn(`[import-download] Invalid SWF magic bytes from ${url}`);
return { ok: false, size: 0 };
return { ok: false, size: 0, error: "Response is not a SWF file" };
}
continue;
}
if (options?.validate === "png" && !validatePngBytes(buffer)) {
if (attempt === maxRetries) {
console.warn(`[import-download] Invalid PNG magic bytes from ${url}`);
return { ok: false, size: 0 };
return { ok: false, size: 0, error: "Response is not a PNG file" };
}
continue;
}
if (options?.validate === "nitro" && !validateNitroBytes(buffer))
return { ok: false, size: 0 };
return { ok: false, size: 0, error: "Invalid Nitro bundle" };
stage = "write";
await fs.writeFile(/*turbopackIgnore: true*/ destPath, buffer);
return { ok: true, size: buffer.length };
} catch (err) {
if (attempt === maxRetries) {
if (attempt === maxRetries || stage === "write") {
console.warn(
`[import-download] Download error ${url}:`,
(err as Error).message,
);
return { ok: false, size: 0 };
const failure = err as {
code?: string;
name?: string;
cause?: { code?: string };
};
const code =
failure.code ?? failure.cause?.code ?? failure.name ?? "unknown";
return {
ok: false,
size: 0,
error: `${stage === "write" ? "Cannot save file" : "Network failure"} (${code})`,
};
}
}
}