paperclipai/paperclip · error

Animation must contain only visual HTML/CSS or inline SVG…

Error message

Animation must contain only visual HTML/CSS or inline SVG; scripts, navigation, resources and interactive elements are not supported

What it means

After DOMPurify sanitizes the animation document with a strict allowlist (visual HTML/CSS and inline SVG only, no data/aria attributes), any element or attribute DOMPurify had to remove causes this error. The animation is treated as a purely visual document — scripts, navigation, remote resources, and interactive elements are categorically rejected rather than silently stripped, so publishers know their content violates the policy.

Solutions

  1. Remove all scripts, event-handler attributes, forms, links, iframes, and external resource references from the animation.
  2. Restrict markup to the visual allowlist: basic HTML/CSS and inline SVG with presentational attributes only (fill, stroke, transform, opacity, etc.).
  3. Move any interactivity out of the animation; announcements are static visual documents.
  4. Test locally by running the same DOMPurify config and checking purifier.removed is empty before publishing.

Example fix

// before
<div class="anim" onclick="play()"><script src="tracker.js"></script><a href="https://x">Go</a></div>
// after
<div class="anim"><svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="currentColor"/></svg></div>
Defensive patterns

Strategy: validation

Validate before calling

import { JSDOM } from "jsdom"; import createDOMPurify from "dompurify";
const dom = new JSDOM("");
const purifier = createDOMPurify(dom.window);
purifier.sanitize(source, { ALLOWED_TAGS, ALLOWED_ATTR, ALLOW_DATA_ATTR: false, ALLOW_ARIA_ATTR: false });
if (purifier.removed.length) throw new Error(`Forbidden content: ${purifier.removed.map(r => r.element ?? r.attribute)}`);
dom.window.close();

Type guard

null

Try / catch

try { await publishAnnouncement({ kind: "animation", bytes }); }
catch (e) {
  if (e.message.startsWith("Animation must contain only visual")) {
    return stripForbiddenMarkup(assetPath); // remove scripts/links/forms, retry
  }
  throw e;
}

Prevention

When it happens

Trigger: Uploading an animation containing script tags, event handlers, form/input elements, links with navigation, url(...) resource loads, iframes, or any attribute outside the ALLOWED_TAGS/ALLOWED_ATTR allowlist that DOMPurify removes during sanitization.

Common situations: Exporting from an animation tool that injects script loaders or data-URI attributes; including <a href> click-throughs or <button> elements in a promo animation; CSS referencing external fonts/images; hand-authored SVG with event attributes like onload.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/36063d3318d62ce3. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/announcement-animation.ts:27

  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");
    }
    return html;
  } finally {
    dom.window.close();
  }
}

View on GitHub (pinned to 3f1d897a7c)