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
- Remove all scripts, event-handler attributes, forms, links, iframes, and external resource references from the animation.
- Restrict markup to the visual allowlist: basic HTML/CSS and inline SVG with presentational attributes only (fill, stroke, transform, opacity, etc.).
- Move any interactivity out of the animation; announcements are static visual documents.
- 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
- Lint animations for script/a/button/iframe tags before publishing
- Avoid event-handler attributes and url() external references in CSS
- Run the same DOMPurify allowlist locally before upload
- Keep animations purely presentational; move interactivity elsewhere
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
- Announcement request failed
- Asset filename must match its SHA-256 digest
- Assets must be a real directory
- External chat text exceeds its projected processing limit
- Invalid or oversized announcement animation
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)