mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
fix(engine): validate remote download integrity (#2938)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user