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

  1. Shrink the animation below 128 KiB — minify CSS/HTML, remove whitespace, split into multiple announcements.
  2. Ensure the file is valid UTF-8 text (convert encodings; never upload binary formats).
  3. Check the build/export step produced a non-empty file before publishing.
  4. 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

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


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)