{"record":{"id":"053145d67e635153","repo":"musistudio/claude-code-router","slug":"plugin-stop-hook-failed-formaterror-error","errorCode":null,"errorMessage":"[plugin] Stop hook failed: ${formatError(error)}","messagePattern":"\\[plugin\\] Stop hook failed: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/core/src/plugins/service.ts","lineNumber":298,"sourceCode":"      try {\n        await this.loadConfiguredPlugin(pluginConfig);\n      } catch (error) {\n        await this.rollbackConfiguredPluginLoad(pluginConfig.id, snapshot);\n        console.warn(`[plugin:${pluginConfig.id}] Disabled after startup failure: ${formatError(error)}`);\n      }\n    }\n  }\n\n  async stop(options: { nextConfig?: AppConfig } = {}): Promise<void> {\n    const stopHooks = [...this.stopHooks].reverse();\n    const nextEnabledPluginIds = options.nextConfig ? enabledPluginIds(options.nextConfig) : undefined;\n    this.stopHooks = [];\n\n    for (const stopHook of stopHooks) {\n      try {\n        await stopHook.stop({ reason: stopReasonForPlugin(stopHook.pluginId, nextEnabledPluginIds) });\n      } catch (error) {\n        console.warn(`[plugin] Stop hook failed: ${formatError(error)}`);\n      }\n    }\n\n    const resourceOwnerIds = [...this.resourceOwnerIds].reverse();\n    this.resourceOwnerIds.clear();\n    for (const ownerId of resourceOwnerIds) {\n      await backendService.stopOwner(ownerId);\n    }\n\n    this.config = undefined;\n    this.apps = [];\n    this.coreGatewayConfig = {};\n    this.coreProviderPlugins = [];\n    this.gatewayRequestTransforms = [];\n    this.gatewayRoutes = [];\n    this.proxyRoutes = [];\n    this.providerAccountConnectors.clear();\n    this.virtualModelProfiles = [];","sourceCodeStart":280,"sourceCodeEnd":316,"githubUrl":"https://github.com/musistudio/claude-code-router/blob/99f24806c6a2c660b16e53e95211c517448a6c90/packages/core/src/plugins/service.ts#L280-L316","documentation":"Warned by GatewayPluginService.stop when an individual plugin stop hook throws while being stopped (reason from stopReasonForPlugin: e.g. 'disabled' or config change). Iteration continues over remaining hooks and resource owners, so one plugin's failure to clean up does not block overall shutdown.","triggerScenarios":"Calling stop or start (which restarts), or loadConfiguredPlugin/rollbackConfiguredPluginLoad, where a plugin's registered stop callback rejects — unclosed server, hung async cleanup, teardown bug.","commonSituations":"A third-party plugin whose stop awaits something that never resolves or throws; shutdown racing in-flight requests handled by the plugin; teardown code assuming resources that were never created.","solutions":["Identify the plugin from the error and update/patch its stop hook.","Report the throw to the plugin author — stop hooks must be defensive and never throw.","If it blocks practical operation (e.g. lingering handles), restart the process after ensuring critical state is flushed."],"exampleFix":"// plugin stop hook best practice\n// before\nstop: async () => { await this.server.close(); } // throws if server never started\n// after\nstop: async () => { if (this.server) await this.server.close().catch(() => {}); }","handlingStrategy":"try-catch","validationCode":"null","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Write stop hooks that never throw and are idempotent","Close only resources you actually created","Test plugin start/stop cycles during development"],"tags":["plugins","shutdown","stop-hook","lifecycle"],"backgroundTag":"plugin-stop-hook-failed","analyzedSha":"99f24806c6a2c660b16e53e95211c517448a6c90","analyzedAt":"2026-08-27T04:11:01.184Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}