{"record":{"id":"8e6b58fcae165c41","repo":"paperclipai/paperclip","slug":"plugin-is-not-ready-current-status-plugin-stat","errorCode":null,"errorMessage":"Plugin is not ready (current status: ${plugin.status})","messagePattern":"Plugin is not ready \\(current status: (.+?)\\)","errorType":"http","errorClass":null,"httpStatus":503,"severity":"error","filePath":"server/src/routes/plugins.ts","lineNumber":1846,"sourceCode":"\n    req.on(\"close\", safeUnsubscribe);\n    res.on(\"error\", safeUnsubscribe);\n  });\n\n  router.use(\"/plugins/:pluginId/api\", async (req, res) => {\n    if (!bridgeDeps) {\n      res.status(501).json({ error: \"Plugin scoped API routes are not enabled\" });\n      return;\n    }\n\n    const { pluginId } = req.params;\n    const plugin = await resolvePlugin(registry, pluginId);\n    if (!plugin) {\n      res.status(404).json({ error: \"Plugin not found\" });\n      return;\n    }\n    if (plugin.status !== \"ready\") {\n      res.status(503).json({ error: `Plugin is not ready (current status: ${plugin.status})` });\n      return;\n    }\n    const isWorkerRunning = typeof bridgeDeps.workerManager.isRunning === \"function\"\n      ? bridgeDeps.workerManager.isRunning(plugin.id)\n      : true;\n    if (!isWorkerRunning) {\n      res.status(503).json({ error: \"Plugin worker is not running\" });\n      return;\n    }\n    if (!plugin.manifestJson.capabilities.includes(\"api.routes.register\")) {\n      res.status(404).json({ error: \"Plugin does not expose scoped API routes\" });\n      return;\n    }\n\n    const requestPath = req.path || \"/\";\n    const routes = plugin.manifestJson.apiRoutes ?? [];\n    const match = routes\n      .map((route) => ({ route, params: matchScopedApiRoute(route, req.method, requestPath) }))","sourceCodeStart":1828,"sourceCodeEnd":1864,"githubUrl":"https://github.com/paperclipai/paperclip/blob/a7e689b3c35347b529cb9f54c9b9a8575a3dcab6/server/src/routes/plugins.ts#L1828-L1864","documentation":"Returned as HTTP 503 by the plugin scoped API gateway (router.use(\"/plugins/:pluginId/api\", ...)) when the plugin record exists in the database but its lifecycle status is not \"ready\". Only ready plugins have a live worker, so the gateway refuses to forward the request. The offending status is embedded in the message (e.g. \"disabled\", \"error\", \"upgrade_pending\", \"installing\", \"uninstalled\").","triggerScenarios":"Any call to /api/plugins/:pluginId/api/* while the plugin row has a non-ready status: after POST /plugins/:id/disable, after a worker crash marked the plugin \"error\", after an upgrade added new capabilities (status \"upgrade_pending\" awaiting operator approval), or mid-install before activation finishes.","commonSituations":"UI or integration hits a plugin's scoped API right after server startup before the loader activates the plugin; plugin was disabled for maintenance; a failed upgrade left it in upgrade_pending; missing plugin config caused an error state while clients keep calling its API routes.","solutions":["GET /api/plugins/:pluginId to read the current status (the status is also named in the error message itself).","If status is disabled, error, or upgrade_pending, have an instance admin POST /api/plugins/:pluginId/enable to transition it back to ready.","If it should already be ready, run GET /api/plugins/:pluginId/health and fix failing checks (manifest validity, config, worker).","In client code treat the 503 as transient only when the plugin is expected to become ready; otherwise surface the status to the operator."],"exampleFix":"// before\nconst res = await fetch(`/api/plugins/${pluginId}/api/issues`);\n\n// after\nconst plugin = await (await fetch(`/api/plugins/${pluginId}`)).json();\nif (plugin.status !== \"ready\") {\n  await fetch(`/api/plugins/${pluginId}/enable`, { method: \"POST\" }); // instance admin\n}\nconst res = await fetch(`/api/plugins/${pluginId}/api/issues`);","handlingStrategy":"validation","validationCode":"async function assertPluginReady(apiBase: string, pluginId: string): Promise<void> {\n  const res = await fetch(`${apiBase}/api/plugins/${encodeURIComponent(pluginId)}`);\n  if (res.status === 404) throw new Error(`Plugin ${pluginId} not found`);\n  const plugin = await res.json();\n  if (plugin.status !== \"ready\") {\n    throw new Error(`Plugin ${pluginId} not ready (status: ${plugin.status}) — enable it first`);\n  }\n}","typeGuard":"interface PluginRecord { id: string; pluginKey: string; status: string }\nfunction isReadyPlugin(v: unknown): v is PluginRecord & { status: \"ready\" } {\n  return (\n    typeof v === \"object\" && v !== null &&\n    \"pluginKey\" in v && \"status\" in v &&\n    (v as PluginRecord).status === \"ready\"\n  );\n}","tryCatchPattern":"try {\n  await callScopedApi(pluginId, path);\n} catch (err) {\n  if (err instanceof HttpError && err.status === 503 && /not ready/i.test(err.message)) {\n    const status = err.message.match(/status: (\\w+)/)?.[1];\n    // reconcile: enable from disabled/error/upgrade_pending, then retry once\n  } else throw err;\n}","preventionTips":["Check plugin.status via GET /api/plugins/:id before calling scoped API routes.","Subscribe to plugin.ui.updated live events to react to status changes instead of discovering them via 503.","Gate plugin UI panels on status === 'ready' so users never reach a route that will 503."],"tags":["plugin","lifecycle","http-503","scoped-api","status"],"backgroundTag":"service-unavailable","analyzedSha":"a7e689b3c35347b529cb9f54c9b9a8575a3dcab6","analyzedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}