{"record":{"id":"e144dbece02d7432","repo":"paperclipai/paperclip","slug":"file-not-found","errorCode":null,"errorMessage":"File not found","messagePattern":"File not found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":432,"sourceCode":"\n    if (!uiDir) {\n      log.warn(\n        { pluginId: plugin.id, pluginKey: plugin.pluginKey, packageName: plugin.packageName },\n        \"plugin-ui-static: UI directory not found on disk\",\n      );\n      res.status(404).json({ error: \"Plugin UI directory not found\" });\n      return;\n    }\n\n    // Step 4: Resolve the requested file path and prevent traversal (including symlinks)\n    const resolvedFilePath = path.resolve(uiDir, rawFilePath);\n\n    // Step 5: Check that the file exists and is a regular file\n    let fileStat: fs.Stats;\n    try {\n      fileStat = fs.statSync(resolvedFilePath);\n    } catch {\n      res.status(404).json({ error: \"File not found\" });\n      return;\n    }\n\n    // Security: resolve symlinks via realpathSync and verify containment.\n    // This prevents symlink-based traversal that string-based startsWith misses.\n    let realFilePath: string;\n    let realUiDir: string;\n    try {\n      realFilePath = fs.realpathSync(resolvedFilePath);\n      realUiDir = fs.realpathSync(uiDir);\n    } catch {\n      res.status(404).json({ error: \"File not found\" });\n      return;\n    }\n\n    const relative = path.relative(realUiDir, realFilePath);\n    if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n      res.status(403).json({ error: \"Access denied\" });","sourceCodeStart":414,"sourceCodeEnd":450,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L414-L450","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"const exists = await fetch(`/_plugins/${pluginId}/ui/${file}`, { method: \"HEAD\" });\nif (!exists.ok) {\n  // Asset not in bundle — re-fetch plugin entry/manifest and skip caching\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch(url);\nif (res.status === 404) {\n  // Bust the cached index.html, re-resolve hashed filenames, then retry once with the fresh manifest\n}","preventionTips":["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"],"tags":["plugins","plugin-ui","static-files","http-404","cache-invalidation"],"backgroundTag":"static-asset-not-found","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}