fix(engine): validate remote download integrity (#2938)

This commit is contained in:
James Russo
2026-08-04 18:02:01 -07:00
committed by GitHub
parent 7bf425b7a9
commit bbdfee1166
11 changed files with 2219 additions and 133 deletions
@@ -52,6 +52,7 @@ describe("processCompositionAudio", () => {
const tempDirs: string[] = [];
afterEach(() => {
vi.unstubAllGlobals();
runFfmpegMock.mockClear();
extractAudioMetadataMock.mockReset();
extractAudioMetadataMock.mockResolvedValue({
@@ -66,6 +67,44 @@ describe("processCompositionAudio", () => {
}
});
it("classifies an HTML-as-200 audio source as deterministic user input", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
tempDirs.push(baseDir, workDir);
const fetchMock = vi
.fn()
.mockResolvedValue(new Response("<!doctype html><html><body>denied</body></html>"));
vi.stubGlobal("fetch", fetchMock);
const result = await processCompositionAudio(
[
{
id: "remote-voice",
src: "https://cdn.example/voice",
start: 0,
end: 2,
mediaStart: 0,
layer: 0,
volume: 1,
type: "audio",
},
],
baseDir,
workDir,
join(baseDir, "out.m4a"),
2,
);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(result.failures).toEqual([
expect.objectContaining({
stage: "download",
owner: "user",
retryable: false,
}),
]);
});
it.each([
{
message: "AbortError: ffprobe operation aborted",
+22 -10
View File
@@ -9,7 +9,12 @@ import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeF
import { join, dirname } from "path";
import { parseHTML } from "linkedom";
import { extractAudioMetadata } from "../utils/ffprobe.js";
import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js";
import {
downloadToTemp,
isHttpUrl,
UrlDownloadError,
writeUrlDownloadTelemetry,
} from "../utils/urlDownloader.js";
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
import { formatFfmpegError, runFfmpeg, type RunFfmpegResult } from "../utils/runFfmpeg.js";
import { unwrapTemplate } from "../utils/htmlTemplate.js";
@@ -241,16 +246,23 @@ function probeFailure(message: string, elementId: string): AudioProcessingFailur
};
}
function downloadFailure(message: string, elementId: string): AudioProcessingFailure {
function downloadFailure(error: unknown, elementId: string): AudioProcessingFailure {
const message = error instanceof Error ? error.message : String(error);
const invalidSource =
/(?:invalid URL|only HTTPS|private\/reserved|HTTP (?:400|401|403|404|405|410|422)\b)/i.test(
message,
);
error instanceof UrlDownloadError
? error.kind === "http_not_found" ||
error.kind === "http_rejected" ||
error.kind === "invalid_payload" ||
error.kind === "cancelled"
: /(?:invalid URL|only HTTPS|private\/reserved|HTTP (?:400|401|403|404|405|410|422)\b)/i.test(
message,
);
const retryable = error instanceof UrlDownloadError ? error.retryable : !invalidSource;
return {
stage: "download",
reason: "download_failed",
owner: invalidSource ? "user" : "system",
retryable: !invalidSource,
retryable,
elementId,
detail: boundedDetail(`Download failed for audio element ${elementId}: ${message}`),
};
@@ -712,11 +724,11 @@ export async function processCompositionAudio(
if (isHttpUrl(srcPath)) {
try {
srcPath = await downloadToTemp(srcPath, workDir);
srcPath = await downloadToTemp(srcPath, workDir, undefined, signal, undefined, {
onTelemetry: writeUrlDownloadTelemetry,
});
} catch (err: unknown) {
failures.push(
downloadFailure(err instanceof Error ? err.message : String(err), element.id),
);
failures.push(downloadFailure(err, element.id));
return;
}
}
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { classifyFfmpegSpawnError } from "./videoFrameExtractor.js";
import { UrlDownloadError } from "../utils/urlDownloader.js";
import { classifyFfmpegSpawnError, classifyVideoExtractionError } from "./videoFrameExtractor.js";
describe("classifyFfmpegSpawnError", () => {
it.each(["ENOENT", "EACCES", "ENOEXEC", "UNKNOWN"])(
@@ -18,3 +19,21 @@ describe("classifyFfmpegSpawnError", () => {
});
});
});
describe("classifyVideoExtractionError download integrity", () => {
it("keeps deterministic HTML payloads non-retryable and user-owned as invalid media", () => {
const classified = classifyVideoExtractionError(
new UrlDownloadError("invalid_payload", false, "HTML payload"),
);
expect(classified).toMatchObject({ kind: "invalid_media", retryable: false });
});
it.each(["range_protocol", "length_mismatch", "hash_mismatch"] as const)(
"keeps %s retryable after the downloader's one clean refetch is exhausted",
(kind) => {
expect(
classifyVideoExtractionError(new UrlDownloadError(kind, true, "integrity failure")),
).toMatchObject({ kind: "download_transient", retryable: true });
},
);
});
@@ -28,7 +28,12 @@ import {
isHdrColorSpace as isHdrColorSpaceUtil,
type HdrTransfer,
} from "../utils/hdr.js";
import { downloadToTemp, isHttpUrl, UrlDownloadError } from "../utils/urlDownloader.js";
import {
downloadToTemp,
isHttpUrl,
UrlDownloadError,
writeUrlDownloadTelemetry,
} from "../utils/urlDownloader.js";
import { runFfmpeg } from "../utils/runFfmpeg.js";
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
import { unwrapTemplate } from "../utils/htmlTemplate.js";
@@ -342,6 +347,14 @@ export function classifyVideoExtractionError(error: unknown): VideoSourceExtract
diagnostic,
);
}
if (error.kind === "invalid_payload") {
return new VideoSourceExtractionError(
"invalid_media",
false,
"Video source download returned a non-media payload",
diagnostic,
);
}
if (error.retryable) {
return new VideoSourceExtractionError(
"download_transient",
@@ -1424,8 +1437,13 @@ export async function extractAllVideoFrames(
if (isHttpUrl(videoPath)) {
const downloadDir = join(options.outputDir, "_downloads");
mkdirSync(downloadDir, { recursive: true });
videoPath = await downloadToTemp(videoPath, downloadDir, undefined, signal, () =>
recordTransientRetries(1),
videoPath = await downloadToTemp(
videoPath,
downloadDir,
undefined,
signal,
() => recordTransientRetries(1),
{ onTelemetry: writeUrlDownloadTelemetry },
);
}