diff --git a/examples/aws-lambda/template.yaml b/examples/aws-lambda/template.yaml index 34b2a0143..7ae0b7f6f 100644 --- a/examples/aws-lambda/template.yaml +++ b/examples/aws-lambda/template.yaml @@ -265,8 +265,8 @@ Resources: - PlanProtocolUnsupportedError - VIDEO_SOURCE_UNRENDERABLE - INVALID_VIDEO_METADATA - - MARKUP_NOT_MEDIA - - MarkupNotMediaError + - NOT_MEDIA_PAYLOAD + - NotMediaPayloadError - PLAN_ARTIFACT_DIGEST_MISMATCH - FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED MaxAttempts: 0 @@ -311,8 +311,8 @@ Resources: - PLAN_V2_INTEGRITY_UNRECOVERABLE - VIDEO_SOURCE_UNRENDERABLE - INVALID_VIDEO_METADATA - - MARKUP_NOT_MEDIA - - MarkupNotMediaError + - NOT_MEDIA_PAYLOAD + - NotMediaPayloadError - PlanV2IntegrityError - PLAN_ARTIFACT_DIGEST_MISMATCH - FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts index defd4372b..c7f27f503 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.snapshot.test.ts @@ -78,8 +78,8 @@ const EXPECTED_NON_RETRYABLE_ERRORS = new Set([ "PLAN_V2_INTEGRITY_UNRECOVERABLE", "VIDEO_SOURCE_UNRENDERABLE", "INVALID_VIDEO_METADATA", - "MARKUP_NOT_MEDIA", - "MarkupNotMediaError", + "NOT_MEDIA_PAYLOAD", + "NotMediaPayloadError", "PlanV2IntegrityError", "PLAN_ARTIFACT_DIGEST_MISMATCH", "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED", diff --git a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts index db74a8271..baa7bd6b1 100644 --- a/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts +++ b/packages/aws-lambda/src/cdk/HyperframesRenderStack.ts @@ -206,8 +206,8 @@ export class HyperframesRenderStack extends Construct { "PLAN_V2_INTEGRITY_UNRECOVERABLE", "VIDEO_SOURCE_UNRENDERABLE", "INVALID_VIDEO_METADATA", - "MARKUP_NOT_MEDIA", - "MarkupNotMediaError", + "NOT_MEDIA_PAYLOAD", + "NotMediaPayloadError", "PlanV2IntegrityError", "PLAN_ARTIFACT_DIGEST_MISMATCH", "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED", diff --git a/packages/aws-lambda/src/handler.ts b/packages/aws-lambda/src/handler.ts index 92fbc0c8b..bd10c12c2 100644 --- a/packages/aws-lambda/src/handler.ts +++ b/packages/aws-lambda/src/handler.ts @@ -152,7 +152,7 @@ function normalizeTerminalErrorName(error: unknown): void { candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || - candidate.code === "MARKUP_NOT_MEDIA" || + candidate.code === "NOT_MEDIA_PAYLOAD" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index d4b9adb4a..5b5f71f7f 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -274,12 +274,12 @@ export { } from "./utils/ffprobe.js"; export { - MARKUP_NOT_MEDIA, - MarkupNotMediaError, - assertNotMarkupPayload, + NOT_MEDIA_PAYLOAD, + NotMediaPayloadError, + assertMediaPayload, fingerprintElementId, - isMarkupPayload, -} from "./utils/markupPayload.js"; + isNotMediaPayload, +} from "./utils/notMediaPayload.js"; export { assertPublicHttpsUrl, downloadToTemp, isHttpUrl } from "./utils/urlDownloader.js"; export { diff --git a/packages/engine/src/services/audioMixer.test.ts b/packages/engine/src/services/audioMixer.test.ts index cb37baa10..96f5a6606 100644 --- a/packages/engine/src/services/audioMixer.test.ts +++ b/packages/engine/src/services/audioMixer.test.ts @@ -120,7 +120,7 @@ describe("processCompositionAudio", () => { // probe entirely when the element carries an authored duration, and used to // surface as `prepare/ffmpeg_failed` with owner "system" — an authoring bug // paged as a platform fault, after every frame had already been captured. - it("classifies a markup audio source as a user-owned invalid media source", async () => { + it("classifies a document audio source as a user-owned invalid media source", async () => { const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-")); const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-")); tempDirs.push(baseDir, workDir); diff --git a/packages/engine/src/services/audioMixer.ts b/packages/engine/src/services/audioMixer.ts index 21b56fa71..f27a9eabb 100644 --- a/packages/engine/src/services/audioMixer.ts +++ b/packages/engine/src/services/audioMixer.ts @@ -9,7 +9,7 @@ import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeF import { join, dirname } from "path"; import { parseHTML } from "linkedom"; import { extractAudioMetadata } from "../utils/ffprobe.js"; -import { isMarkupPayload } from "../utils/markupPayload.js"; +import { isNotMediaPayload } from "../utils/notMediaPayload.js"; import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js"; import { DEFAULT_CONFIG, type EngineConfig } from "../config.js"; import { formatFfmpegError, runFfmpeg, type RunFfmpegResult } from "../utils/runFfmpeg.js"; @@ -734,13 +734,13 @@ export async function processCompositionAudio( return; } - // STUDIO-5433: an audio src that resolved to an HTML/XML document (an + // STUDIO-5433: an audio src that resolved to a text document (an // unresolved nested-composition preview URL, or a 403/404 body served // with a 200) never reaches the probe below when the element carries an // authored duration or `loop`. It then fails inside ffmpeg as // `prepare/ffmpeg_failed` with owner "system" — an authoring bug paged // as a platform fault, after every frame has already been captured. - if (await isMarkupPayload(srcPath)) { + if (await isNotMediaPayload(srcPath)) { failures.push({ stage: "source", reason: "invalid_media", @@ -748,7 +748,7 @@ export async function processCompositionAudio( retryable: false, elementId: element.id, detail: boundedDetail( - `Audio element ${element.id} source is a markup document (HTML/XML), not media`, + `Audio element ${element.id} source is a text document (HTML/XML/JSON), not media`, ), }); return; diff --git a/packages/engine/src/utils/markupPayload.test.ts b/packages/engine/src/utils/notMediaPayload.test.ts similarity index 70% rename from packages/engine/src/utils/markupPayload.test.ts rename to packages/engine/src/utils/notMediaPayload.test.ts index db90cf3e4..bdfdb844a 100644 --- a/packages/engine/src/utils/markupPayload.test.ts +++ b/packages/engine/src/utils/notMediaPayload.test.ts @@ -3,11 +3,11 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { - assertNotMarkupPayload, + assertMediaPayload, fingerprintElementId, - isMarkupPayload, - MarkupNotMediaError, -} from "./markupPayload.js"; + isNotMediaPayload, + NotMediaPayloadError, +} from "./notMediaPayload.js"; function writeFixture(name: string, contents: string | Buffer): string { const dir = mkdtempSync(join(tmpdir(), "hf-markup-sniff-")); @@ -16,7 +16,7 @@ function writeFixture(name: string, contents: string | Buffer): string { return filePath; } -describe("isMarkupPayload", () => { +describe("isNotMediaPayload", () => { it.each([ ["doctype", "\n"], ["bare html tag, uppercase", "hi"], @@ -31,11 +31,15 @@ describe("isMarkupPayload", () => { // signed URL or an ACL change, served as a 200 with an S3 error body. ["s3 error document", 'AccessDenied'], ["comment first", "\n"], + // Replicate answers a dead asset this way, and a gateway in front of it can + // relay the body with a 200 — the shape a `<` -only check misses. + ["json object error body", '{"detail": "requested file not found"}'], + ["json array body", '[{"error": "gone"}]'], ])("detects %s", async (_label, contents) => { - expect(await isMarkupPayload(writeFixture("payload", contents))).toBe(true); + expect(await isNotMediaPayload(writeFixture("payload", contents))).toBe(true); }); - it("detects markup behind a UTF-8 BOM and leading whitespace", async () => { + it("detects a document behind a UTF-8 BOM and leading whitespace", async () => { const filePath = writeFixture( "bom.html", Buffer.concat([ @@ -43,15 +47,15 @@ describe("isMarkupPayload", () => { Buffer.from("\n \t"), ]), ); - expect(await isMarkupPayload(filePath)).toBe(true); + expect(await isNotMediaPayload(filePath)).toBe(true); }); - it("detects markup behind a leading NUL run", async () => { + it("detects a document behind a leading NUL run", async () => { const filePath = writeFixture( "nul.html", Buffer.concat([Buffer.from([0x00, 0x00, 0x00]), Buffer.from("x")]), ); - expect(await isMarkupPayload(filePath)).toBe(true); + expect(await isNotMediaPayload(filePath)).toBe(true); }); it.each([ @@ -63,15 +67,15 @@ describe("isMarkupPayload", () => { const body = Buffer.from("", "utf16le"); const bytes = _label === "little-endian" ? body : body.swap16(); const filePath = writeFixture("utf16.html", Buffer.concat([Buffer.from(bom), bytes])); - expect(await isMarkupPayload(filePath)).toBe(true); + expect(await isNotMediaPayload(filePath)).toBe(true); }); - it("detects markup preceded by more whitespace than a short read would cover", async () => { + it("detects nothing when padding overruns the sniff window", async () => { const filePath = writeFixture("padded.html", `${" ".repeat(600)}`); // 600 bytes of padding overruns the 512-byte sniff window, so the verdict // has to be "unknown" (false) rather than a misread — asserted here so the // window size is a deliberate, visible bound rather than an accident. - expect(await isMarkupPayload(filePath)).toBe(false); + expect(await isNotMediaPayload(filePath)).toBe(false); }); it.each([ @@ -85,50 +89,50 @@ describe("isMarkupPayload", () => { ["adts aac", [0xff, 0xf1, 0x50, 0x80]], ["mpeg-ps", [0x00, 0x00, 0x01, 0xba]], ])("does not flag a %s container", async (_label, bytes) => { - expect(await isMarkupPayload(writeFixture("clip.bin", Buffer.from(bytes)))).toBe(false); + expect(await isNotMediaPayload(writeFixture("clip.bin", Buffer.from(bytes)))).toBe(false); }); - it("does not flag a container that merely contains markup further in", async () => { + it("does not flag a container that merely contains a document byte further in", async () => { const filePath = writeFixture( "not-html.bin", Buffer.concat([Buffer.from([0x00, 0x00, 0x01, 0xba]), Buffer.from(" { - expect(await isMarkupPayload(writeFixture("empty.bin", ""))).toBe(false); + expect(await isNotMediaPayload(writeFixture("empty.bin", ""))).toBe(false); }); - it("reports not-markup instead of throwing when the path is a directory", async () => { + it("reports not-a-document instead of throwing when the path is a directory", async () => { // `existsSync` passes for a directory, so callers reach the sniff with one. // The read fails EISDIR; classifying rather than propagating keeps the real // probe's own error as the one the caller sees. const dir = mkdtempSync(join(tmpdir(), "hf-markup-sniff-dir-")); mkdirSync(join(dir, "assets")); - expect(await isMarkupPayload(join(dir, "assets"))).toBe(false); + expect(await isNotMediaPayload(join(dir, "assets"))).toBe(false); }); - it("reports not-markup instead of throwing when the file is missing", async () => { + it("reports not-a-document instead of throwing when the file is missing", async () => { const dir = mkdtempSync(join(tmpdir(), "hf-markup-sniff-gone-")); - expect(await isMarkupPayload(join(dir, "evicted.mp4"))).toBe(false); + expect(await isNotMediaPayload(join(dir, "evicted.mp4"))).toBe(false); }); }); -describe("assertNotMarkupPayload", () => { - it("throws MarkupNotMediaError carrying routing metadata and a hashed element key", async () => { +describe("assertMediaPayload", () => { + it("throws NotMediaPayloadError carrying routing metadata and a hashed element key", async () => { const filePath = writeFixture("nested.html", ""); let caught: unknown; try { - await assertNotMarkupPayload(filePath, "aroll-scene-3"); + await assertMediaPayload(filePath, "aroll-scene-3"); } catch (error) { caught = error; } - expect(caught).toBeInstanceOf(MarkupNotMediaError); - const error = caught as MarkupNotMediaError; - expect(error.code).toBe("MARKUP_NOT_MEDIA"); + expect(caught).toBeInstanceOf(NotMediaPayloadError); + const error = caught as NotMediaPayloadError; + expect(error.code).toBe("NOT_MEDIA_PAYLOAD"); expect(error.owner).toBe("user"); expect(error.retryable).toBe(false); expect(error.elementFingerprints).toEqual([fingerprintElementId("aroll-scene-3")]); @@ -145,26 +149,26 @@ describe("assertNotMarkupPayload", () => { '', ); - const error = await assertNotMarkupPayload( + const error = await assertMediaPayload( filePath, "https://cdn.example.com/tenants/acme-corp/projects/secret-q4/streamed-preview.html", - ).catch((caught: unknown) => caught as MarkupNotMediaError); + ).catch((caught: unknown) => caught as NotMediaPayloadError); expect(error.message).not.toContain("acme-corp"); expect(error.message).not.toContain("secret-q4"); expect(error.message).not.toContain("cdn.example.com"); expect(error.message).not.toContain("tok_9fA3xQ7pLz"); expect(error.message).toContain("unresolved"); - expect(error.message).toContain("403/404"); + expect(error.message).toContain("success status"); }); it("resolves for a real container", async () => { const filePath = writeFixture("clip.mp4", Buffer.from([0x00, 0x00, 0x00, 0x18, 0x66, 0x74])); - await expect(assertNotMarkupPayload(filePath, "v1")).resolves.toBeUndefined(); + await expect(assertMediaPayload(filePath, "v1")).resolves.toBeUndefined(); }); it("caps the fingerprint list so the message stays bounded", async () => { - const error = new MarkupNotMediaError( + const error = new NotMediaPayloadError( Array.from({ length: 12 }, (_unused, index) => fingerprintElementId(`el-${index}`)), ); expect(error.message).toContain("+4"); diff --git a/packages/engine/src/utils/markupPayload.ts b/packages/engine/src/utils/notMediaPayload.ts similarity index 56% rename from packages/engine/src/utils/markupPayload.ts rename to packages/engine/src/utils/notMediaPayload.ts index 5cbac187e..446e37ad0 100644 --- a/packages/engine/src/utils/markupPayload.ts +++ b/packages/engine/src/utils/notMediaPayload.ts @@ -1,14 +1,14 @@ import { createHash } from "node:crypto"; import { open as openFile } from "node:fs/promises"; -export const MARKUP_NOT_MEDIA = "MARKUP_NOT_MEDIA" as const; +export const NOT_MEDIA_PAYLOAD = "NOT_MEDIA_PAYLOAD" as const; /** Cap the joined fingerprint list so the message stays bounded. */ const MAX_LISTED_FINGERPRINTS = 8; /** - * Thrown when a file behind a `