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

  1. Hard-refresh (or bust the cache) so a fresh index.html referencing the current hashed filenames is fetched
  2. Verify the file actually exists inside the plugin's dist/ui directory on the server
  3. Fix the requesting code to build paths relative to the bundle root exactly as the bundler emits them (no doubled 'dist/ui/' prefix)
  4. 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

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


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/e144dbece02d7432. Report an issue: GitHub.