paperclipai/paperclip · error
File not found
Error message
File not found
What it means
Returned as HTTP 404 by GET /_plugins/:pluginId/ui/* (server/src/routes/plugin-ui-static.ts:432). After resolving uiDir + rawFilePath, the route calls fs.statSync on the result; if stat throws ( overwhelmingly ENOENT — the file does not exist), the client gets a plain 404 before any symlink/containment checks run.
Source
Thrown at server/src/routes/plugin-ui-static.ts:432
if (!uiDir) {
log.warn(
{ pluginId: plugin.id, pluginKey: plugin.pluginKey, packageName: plugin.packageName },
"plugin-ui-static: UI directory not found on disk",
);
res.status(404).json({ error: "Plugin UI directory not found" });
return;
}
// Step 4: Resolve the requested file path and prevent traversal (including symlinks)
const resolvedFilePath = path.resolve(uiDir, rawFilePath);
// Step 5: Check that the file exists and is a regular file
let fileStat: fs.Stats;
try {
fileStat = fs.statSync(resolvedFilePath);
} catch {
res.status(404).json({ error: "File not found" });
return;
}
// Security: resolve symlinks via realpathSync and verify containment.
// This prevents symlink-based traversal that string-based startsWith misses.
let realFilePath: string;
let realUiDir: string;
try {
realFilePath = fs.realpathSync(resolvedFilePath);
realUiDir = fs.realpathSync(uiDir);
} catch {
res.status(404).json({ error: "File not found" });
return;
}
const relative = path.relative(realUiDir, realFilePath);
if (relative.startsWith("..") || path.isAbsolute(relative)) {
res.status(403).json({ error: "Access denied" });View on GitHub (pinned to 120ae5428f)
Solutions
- Hard-refresh (or bust the cache) so a fresh index.html referencing the current hashed filenames is fetched
- Verify the file actually exists inside the plugin's dist/ui directory on the server
- Fix the requesting code to build paths relative to the bundle root exactly as the bundler emits them (no doubled 'dist/ui/' prefix)
- If the plugin was just upgraded, re-fetch its manifest/entry to pick up new asset names
Defensive patterns
Strategy: fallback
Validate before calling
const exists = await fetch(`/_plugins/${pluginId}/ui/${file}`, { method: "HEAD" });
if (!exists.ok) {
// Asset not in bundle — re-fetch plugin entry/manifest and skip caching
} Try / catch
const res = await fetch(url);
if (res.status === 404) {
// Bust the cached index.html, re-resolve hashed filenames, then retry once with the fresh manifest
} Prevention
- Revalidate index.html (the route already sends max-age=0, must-revalidate) — never cache it long
- Only reference asset filenames taken from the freshly loaded entry document
- On plugin upgrade, drop cached chunk URLs immediately
When it happens
Trigger: Requesting an asset that is not in the bundle: /_plugins/<id>/ui/assets/chunk-deadbeef.js after the plugin was rebuilt and chunk hashes changed, a stale index.html (served with must-revalidate) referencing old hashed filenames, or a typo'd/extra path segment like /ui/dist/ui/index.js.
Common situations: Browser or CDN serving a cached index.html that points at renamed content-hashed chunks after a plugin upgrade; SPA base-path misconfiguration prepending an extra directory; hand-built URLs assuming files that the bundler never emitted.
Related errors
- Plugin UI is not available (status: ${plugin.status})
- Plugin does not declare a UI bundle
- Plugin UI directory not found
- Access denied
- Failed to serve file
AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18).
Data as JSON: /api/errors/e144dbece02d7432.
Report an issue: GitHub.