{"record":{"id":"614e8614ccbb8cff","repo":"paperclipai/paperclip","slug":"plugin-ui-is-not-available-status-plugin-statu","errorCode":null,"errorMessage":"Plugin UI is not available (status: ${plugin.status})","messagePattern":"Plugin UI is not available \\(status: (.+?)\\)","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"server/src/routes/plugin-ui-static.ts","lineNumber":271,"sourceCode":"          ? (error as { code?: unknown }).code\n          : undefined;\n      if (maybeCode !== \"22P02\") {\n        throw error;\n      }\n    }\n    if (!plugin) {\n      plugin = await registry.getByKey(pluginId);\n    }\n\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n\n    // Step 2: Verify the plugin is ready and has UI declared\n    if (plugin.status !== \"ready\") {\n      res.status(403).json({\n        error: `Plugin UI is not available (status: ${plugin.status})`,\n      });\n      return;\n    }\n\n    const manifest = plugin.manifestJson;\n    if (!manifest?.entrypoints?.ui) {\n      res.status(404).json({ error: \"Plugin does not declare a UI bundle\" });\n      return;\n    }\n\n    const rawCompanyId = req.query.companyId;\n    if (\n      Array.isArray(rawCompanyId) ||\n      (rawCompanyId !== undefined && typeof rawCompanyId !== \"string\")\n    ) {\n      throw badRequest('\"companyId\" must be a string when provided');\n    }\n    const companyId = typeof rawCompanyId === \"string\" ? rawCompanyId.trim() : \"\";","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/server/src/routes/plugin-ui-static.ts#L253-L289","documentation":"Returned as HTTP 403 by the plugin UI static route (GET /_plugins/:pluginId/ui/*, server/src/routes/plugin-ui-static.ts:271). The route resolves the plugin by UUID or key, but only serves its UI bundle when the registry status is exactly 'ready'. Any other status ('installed', 'disabled', 'error', 'upgrade_pending', 'uninstalled') means the bundle on disk is not guaranteed complete, so serving is refused.","triggerScenarios":"Requesting GET /_plugins/<pluginId>/ui/index.js (or any asset) while GET /api/plugins shows the plugin in a non-ready state: install still in flight ('installed'), install/build failed ('error'), mid-upgrade ('upgrade_pending'), disabled, or uninstalled. Typical when the UI tries to dynamically import the plugin bundle immediately after an install API call returns, before the registry flips the row to 'ready'.","commonSituations":"Polling a freshly installed plugin's UI too early; plugin card still rendered in the board after the plugin was disabled or uninstalled; an upgrade left the plugin in 'upgrade_pending'; install failed (status 'error') but a stale browser tab keeps requesting its assets.","solutions":["Check current status with GET /api/plugins (or GET /api/plugins?status=ready) and only load UI assets once the plugin reports 'ready'","If status is 'error', inspect the install logs and re-run the install/repair flow to move it back to 'ready'","If status is 'disabled' or 'uninstalled', re-enable or reinstall the plugin via the plugins API before loading its UI","In host UI code, gate the dynamic import of the plugin UI entry on plugin.status === 'ready', and on a 403 re-fetch the plugin record instead of blindly retrying the asset URL"],"exampleFix":"// before\nconst mod = await import(`/_plugins/${pluginId}/ui/${entry}`);\n\n// after\nconst plugin = await api.getPlugin(pluginId);\nif (plugin.status !== \"ready\") {\n  throw new Error(`Plugin ${pluginId} not ready (status: ${plugin.status})`);\n}\nconst mod = await import(`/_plugins/${pluginId}/ui/${entry}`);","handlingStrategy":"validation","validationCode":"const res = await fetch(`/api/plugins/${encodeURIComponent(pluginId)}`);\nconst plugin = await res.json();\nif (plugin.status !== \"ready\") {\n  // Defer loading UI assets until the plugin is ready\n  throw new Error(`Plugin not ready: ${plugin.status}`);\n}","typeGuard":"const isPluginReady = (\n  p: { status: string },\n): p is { status: \"ready\" } => p.status === \"ready\";","tryCatchPattern":"const assetRes = await fetch(url);\nif (assetRes.status === 403) {\n  const { error } = await assetRes.json();\n  if (error.startsWith(\"Plugin UI is not available\")) {\n    // Re-fetch plugin status and back off instead of retrying the asset\n  }\n}","preventionTips":["Poll GET /api/plugins until status is 'ready' before importing the plugin UI bundle","Gate extension-slot mounting on plugin.status === 'ready' AND a declared entrypoints.ui","On 403 from /_plugins/:id/ui/*, invalidate cached plugin state and re-fetch rather than retrying the same URL"],"tags":["plugins","plugin-ui","http-403","lifecycle","static-files"],"backgroundTag":"plugin-not-ready","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}