paperclipai/paperclip · error
Invalid or oversized announcement animation
Error message
Invalid or oversized announcement animation
What it means
validateAnnouncementAnimation checks an uploaded announcement animation before sanitization: it must be non-empty and at most ANNOUNCEMENT_ANIMATION_MAX_BYTES (128 KiB), and must decode as strict UTF-8. Anything empty, oversized, or not valid UTF-8 is rejected before DOMPurify ever runs, bounding sanitize work and preventing abuse via giant or binary uploads.
Solutions
- Shrink the animation below 128 KiB — minify CSS/HTML, remove whitespace, split into multiple announcements.
- Ensure the file is valid UTF-8 text (convert encodings; never upload binary formats).
- Check the build/export step produced a non-empty file before publishing.
- Re-run scripts/publish-announcements.ts after fixing the asset; it enforces the same limit locally via prepareAnnouncementPublish.
Example fix
// before
const bytes = new Uint8Array(await fs.readFile("animation.bin"));
await publishAnnouncement({ kind: "animation", bytes }); // throws
// after
const bytes = new Uint8Array(await fs.readFile("animation.html"));
if (!bytes.length || bytes.byteLength > 128 * 1024) {
throw new Error("animation must be 1..131072 bytes of UTF-8 HTML/CSS");
}
await publishAnnouncement({ kind: "animation", bytes }); Defensive patterns
Strategy: validation
Validate before calling
const bytes = new Uint8Array(buf);
if (!bytes.length || bytes.byteLength > 128 * 1024) {
throw new Error(`animation must be 1..${128*1024} bytes`);
}
new TextDecoder("utf-8", { fatal: true }).decode(bytes); // throws on non-UTF-8 Type guard
null
Try / catch
try { await publishAnnouncement({ kind: "animation", bytes }); }
catch (e) {
if (e.message === "Invalid or oversized announcement animation") {
return minifyAndRetry(assetPath); // compress, re-encode UTF-8
}
throw e;
} Prevention
- Check file size and UTF-8 validity before publishing
- Minify animation HTML/CSS to stay under 128 KiB
- Never upload binary formats as animations
- Use scripts/publish-announcements.ts which enforces limits locally
When it happens
Trigger: Publishing an announcement whose animation asset is 0 bytes, exceeds 128 KiB, or contains bytes that are not valid UTF-8 (e.g. a binary file, gzip archive, or latin-1 encoded HTML).
Common situations: Uploading a minified CSS/HTML bundle that quietly grew past 128 KiB; accidentally uploading a .zip/.gif as the animation; tooling that writes the file in a non-UTF-8 encoding; an empty artifact from a failed export step.
Understand the failure class
Background: "File too large" / "file size exceeds limit" errors: why libraries cap file sizes and how to fix them — this error's family across 46 libraries.
Related errors
- Assets must be a real directory
- Invalid or oversized current.json
- Invalid or oversized
- Public protocol eval file exceeds its size boundary
- That file is too large. Choose a GitHub App private key…
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/00f78482b44936fa.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/announcement-animation.ts:10
import createDOMPurify from "dompurify";
import { JSDOM } from "jsdom";
import { ANNOUNCEMENT_ANIMATION_MAX_BYTES } from "@paperclipai/shared";
// A visual HTML/CSS document, never an application. JSDOM does not execute
// scripts or load resources. DOMPurify handles HTML parsing/normalization;
// CSP on delivery also blocks all network requests, including CSS URLs.
export function validateAnnouncementAnimation(bytes: Uint8Array): string {
if (!bytes.length || bytes.byteLength > ANNOUNCEMENT_ANIMATION_MAX_BYTES) {
throw new Error("Invalid or oversized announcement animation");
}
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const dom = new JSDOM("");
try {
const purifier = createDOMPurify(dom.window as unknown as Parameters<typeof createDOMPurify>[0]);
const html = purifier.sanitize(source, {
WHOLE_DOCUMENT: true,
ALLOWED_TAGS: ["html", "head", "body", "style", "div", "span", "p", "br", "strong", "em", "b", "i",
"svg", "g", "path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text", "tspan", "title", "desc"],
ALLOWED_ATTR: ["class", "id", "style", "viewBox", "xmlns", "width", "height", "x", "y", "x1", "x2", "y1", "y2",
"cx", "cy", "r", "rx", "ry", "d", "points", "fill", "stroke", "stroke-width", "stroke-linecap",
"stroke-linejoin", "stroke-dasharray", "stroke-dashoffset", "opacity", "transform", "text-anchor"],
ALLOW_DATA_ATTR: false,
ALLOW_ARIA_ATTR: false,
});
if (purifier.removed.length) {
throw new Error("Animation must contain only visual HTML/CSS or inline SVG; scripts, navigation, resources and interactive elements are not supported");
}View on GitHub (pinned to 3f1d897a7c)