fix(producer): transport safe extraction failure metadata (#3592)

* fix(producer): transport safe extraction failure metadata

* refactor(producer): generalize public error metadata

* test(producer): use vendor-neutral media hosts
This commit is contained in:
James Russo
2026-09-01 21:35:40 -04:00
committed by GitHub
parent 305de15432
commit c0887b650d
12 changed files with 641 additions and 13 deletions
+5
View File
@@ -197,6 +197,7 @@ export {
classifyVideoExtractionError,
isVideoSourceExtractionError,
runVideoExtractionWithRetry,
safeVideoExtractionSourceIdentity,
VideoSourceExtractionError,
type VideoElement,
type ImageElement,
@@ -207,6 +208,10 @@ export {
type TimelineExtractionWindow,
type VideoExtractionFailure,
type VideoExtractionFailureKind,
type VideoExtractionFailureGroupDetails,
type VideoExtractionFailureRetry,
type VideoExtractionFailureStatusClass,
type SafeVideoExtractionSourceIdentity,
type VideoFrameFormat,
VIDEO_FRAME_FORMATS,
isVideoFrameFormat,
@@ -1,6 +1,26 @@
import { describe, expect, it } from "vitest";
import { UrlDownloadError } from "../utils/urlDownloader.js";
import { classifyFfmpegSpawnError, classifyVideoExtractionError } from "./videoFrameExtractor.js";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { safeDownloadUrlIdentity, UrlDownloadError } from "../utils/urlDownloader.js";
import {
classifyFfmpegSpawnError,
classifyVideoExtractionError,
extractAllVideoFrames,
} from "./videoFrameExtractor.js";
const tempDirs: string[] = [];
afterEach(() => {
vi.unstubAllGlobals();
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
function extractionOutputDir(): string {
const dir = mkdtempSync(join(tmpdir(), "hf-extraction-failure-"));
tempDirs.push(dir);
return dir;
}
describe("classifyFfmpegSpawnError", () => {
it.each(["ENOENT", "EACCES", "ENOEXEC", "UNKNOWN"])(
@@ -37,3 +57,74 @@ describe("classifyVideoExtractionError download integrity", () => {
},
);
});
describe("extractAllVideoFrames download failure metadata", () => {
it("threads a query-free fingerprint, allowlisted host, and zero used retries", async () => {
const source =
"https://media.customer-cdn.example/private/clip.mp4?X-Amz-Signature=super-secret#fragment";
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(new Response("<!doctype html><html><body>denied</body></html>")),
);
const result = await extractAllVideoFrames(
[
{
id: "private-video-id",
src: source,
start: 0,
end: 1,
mediaStart: 0,
playbackRate: 1,
loop: false,
hasAudio: false,
},
],
extractionOutputDir(),
{ fps: 30, outputDir: extractionOutputDir() },
);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]?.group).toEqual({
sourceFingerprint: `sha256:${safeDownloadUrlIdentity(source).urlFingerprint}`,
host: "media.customer-cdn.example",
statusClass: "other",
retry: { phase: "download", used: 0, budget: 1 },
});
expect(JSON.stringify(result.errors[0]?.group)).not.toContain("super-secret");
expect(JSON.stringify(result.errors[0]?.group)).not.toContain("private-video-id");
});
it("collapses an exhausted HTTP retry to status class and normalized host", async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 503 }));
vi.stubGlobal("fetch", fetchMock);
const result = await extractAllVideoFrames(
[
{
id: "v1",
src: "https://customer-cdn.example/clip.mp4?token=secret",
start: 0,
end: 1,
mediaStart: 0,
playbackRate: 1,
loop: false,
hasAudio: false,
},
],
extractionOutputDir(),
{ fps: 30, outputDir: extractionOutputDir() },
);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(result.errors[0]).toMatchObject({
kind: "download_transient",
retryable: true,
group: {
host: "customer-cdn.example",
statusClass: "http_5xx",
retry: { phase: "download", used: 1, budget: 1 },
},
});
});
});
@@ -36,6 +36,7 @@ import {
import {
downloadToTemp,
isHttpUrl,
safeDownloadUrlIdentity,
UrlDownloadError,
writeUrlDownloadTelemetry,
} from "../utils/urlDownloader.js";
@@ -280,6 +281,8 @@ export interface VideoExtractionFailure {
kind?: VideoExtractionFailureKind;
/** Always populated by this engine version; absent legacy values fail closed. */
retryable?: boolean;
/** Bounded, path-free grouping data safe for producer-owned error metadata. */
group?: VideoExtractionFailureGroupDetails;
/**
* Operator diagnostic retained inside the engine result. Producer-facing
* errors must summarize `kind`/counts and must not forward this field: it
@@ -288,6 +291,71 @@ export interface VideoExtractionFailure {
error: string;
}
export type VideoExtractionFailureStatusClass =
| "http_4xx"
| "http_5xx"
| "timeout"
| "network"
| "other";
export interface VideoExtractionFailureRetry {
phase: "download";
used: 0 | 1;
budget: 1;
}
export interface VideoExtractionFailureGroupDetails {
sourceFingerprint?: string;
host?: string;
statusClass?: VideoExtractionFailureStatusClass;
retry?: VideoExtractionFailureRetry;
}
export interface SafeVideoExtractionSourceIdentity {
sourceFingerprint: string;
host: string;
}
/** Query/fragment-free remote identity safe for extraction logs and wire metadata. */
export function safeVideoExtractionSourceIdentity(
source: string,
): SafeVideoExtractionSourceIdentity | null {
if (!isHttpUrl(source)) return null;
const identity = safeDownloadUrlIdentity(source);
return {
sourceFingerprint: `sha256:${identity.urlFingerprint}`,
host: identity.host ?? "other",
};
}
function downloadStatusClass(error: UrlDownloadError): VideoExtractionFailureStatusClass {
const status = error.status ?? error.telemetry?.status;
if (typeof status === "number" && status >= 400 && status < 500) return "http_4xx";
if (typeof status === "number" && status >= 500 && status < 600) return "http_5xx";
if (error.kind === "timeout") return "timeout";
if (error.kind === "network") return "network";
return "other";
}
function downloadFailureGroup(
source: string,
error: UrlDownloadError,
): VideoExtractionFailureGroupDetails {
const sourceIdentity = safeVideoExtractionSourceIdentity(source);
const failureHost = error.telemetry?.finalHost ?? error.telemetry?.initialHost;
const attempt = error.telemetry?.attempt;
return {
...(sourceIdentity ? { sourceFingerprint: sourceIdentity.sourceFingerprint } : {}),
...((failureHost ?? sourceIdentity?.host) ? { host: failureHost ?? sourceIdentity?.host } : {}),
statusClass: downloadStatusClass(error),
retry: {
phase: "download",
used: typeof attempt === "number" && attempt >= 2 ? 1 : 0,
budget: 1,
},
};
}
export class VideoSourceExtractionError extends Error {
readonly hyperframesVideoSourceExtractionError = true as const;
@@ -1546,6 +1614,7 @@ export async function extractAllVideoFrames(
videoId: video.id,
kind: classified.kind,
retryable: classified.retryable,
...(err instanceof UrlDownloadError ? { group: downloadFailureGroup(video.src, err) } : {}),
error: classified.diagnostic,
});
}
@@ -447,6 +447,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
).rejects.toMatchObject({
kind: "length_mismatch",
retryable: true,
telemetry: expect.objectContaining({ attempt: 2 }),
} satisfies Partial<UrlDownloadError>);
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(readdirSync(dir).filter((name) => name.startsWith("download_"))).toEqual([]);
@@ -802,6 +803,7 @@ describe("downloadToTemp atomic publication and bounded retry", () => {
).rejects.toMatchObject({
kind: "invalid_payload",
retryable: false,
telemetry: expect.objectContaining({ attempt: 1 }),
} satisfies Partial<UrlDownloadError>);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(temporaryDownloadEntries(dir)).toEqual([]);
+16 -1
View File
@@ -1113,7 +1113,22 @@ async function downloadWithRetry(
return await runDownloadAttempt(url, localPath, timeoutMs, attempt + 1, options, signal);
} catch (error) {
const classified = classifyDownloadFailure(error);
if (!classified.locallyRetryable || attempt >= maxTransientRetries) throw classified;
if (!classified.locallyRetryable || attempt >= maxTransientRetries) {
const identity = safeDownloadUrlIdentity(url);
throw new UrlDownloadError(
classified.kind,
classified.retryable,
classified.message,
classified.status,
{
...classified.telemetry,
urlFingerprint: identity.urlFingerprint,
initialHost: identity.host,
attempt: attempt + 1,
},
classified.locallyRetryable,
);
}
if (classified.retryable) onTransientRetry?.(classified);
const identity = safeDownloadUrlIdentity(url);
emitDownloadTelemetry(options, {