{"record":{"id":"25eba75e0d997f03","repo":"Stirling-Tools/Stirling-PDF","slug":"backend-did-not-become-healthy-after-restart","errorCode":null,"errorMessage":"Backend did not become healthy after restart","messagePattern":"Backend did not become healthy after restart","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"frontend/editor/src/desktop/services/tauriBackendService.ts","lineNumber":133,"sourceCode":"      title: \"Backend stopped unexpectedly\",\n      body: `Attempting to restart... (${this.restartAttempts}/${TauriBackendService.MAX_RESTART_ATTEMPTS})`,\n      durationMs: 5000,\n    });\n    this.isRecovering = true;\n    // Reset started flag so startBackend() will run again\n    this.backendStarted = false;\n    this.startPromise = null;\n    // Fresh grace window: the restarted backend needs boot time before failed\n    // health checks may count as unhealthy again.\n    this.hasBeenHealthy = false;\n    this.setStatus(\"starting\");\n    try {\n      await this.startBackend();\n      // startBackend resolves once the port is known, not once Spring is up -\n      // only declare success after a real health check passes.\n      const healthy = await this.waitUntilHealthy(60_000);\n      if (!healthy) {\n        throw new Error(\"Backend did not become healthy after restart\");\n      }\n      this.restartAttempts = 0; // Reset on successful restart\n      this.isRecovering = false;\n      console.log(\"[TauriBackendService] Backend restarted successfully.\");\n      alert({\n        alertType: \"success\",\n        title: \"Backend restarted\",\n        body: \"The local backend is back online.\",\n        durationMs: 4000,\n      });\n    } catch (err) {\n      console.error(\"[TauriBackendService] Restart failed:\", err);\n      // Set isRecovering = false BEFORE setStatus to prevent re-triggering scheduleRecovery\n      // if the max attempts check above doesn't catch it next time.\n      this.isRecovering = false;\n      if (this.restartAttempts < TauriBackendService.MAX_RESTART_ATTEMPTS) {\n        this.setStatus(\"unhealthy\"); // Will trigger another scheduleRecovery\n      } else {","sourceCodeStart":115,"sourceCodeEnd":151,"githubUrl":"https://github.com/Stirling-Tools/Stirling-PDF/blob/9ef20dcab80b85041912f045e17a6aea1d08f969/frontend/editor/src/desktop/services/tauriBackendService.ts#L115-L151","documentation":"Thrown by TauriBackendService during automated crash recovery after startBackend() resolved (port is known) but waitUntilHealthy(60_000) returned false — meaning the /health (or equivalent) probe never returned a healthy status within the 60-second window. It is wrapped in the recovery try/catch, so it triggers another restart attempt (up to MAX_RESTART_ATTEMPTS = 3) and is not surfaced directly to the caller.","triggerScenarios":"Backend was marked unhealthy (scheduleRecovery invoked), restartAttempts < 3, startBackend() succeeds (port assigned) but the health endpoint never responds 200/healthy within 60s. Causes: the Spring context fails to finish booting (port binding succeeded but app context errored), a dependency (DB, LibreOffice) is missing, or the health endpoint path/expected body changed.","commonSituations":"Bundled backend's required native dependency (LibreOffice, PDFTk) is missing on the user's machine so the Spring context stalls; the health check URL or expected status string drifted from what waitUntilHealthy expects; the machine is slow and 60s is insufficient; a port conflict causes a partial bind.","solutions":["Inspect the bundled backend's stdout/stderr (Tauri sidecar logs) for the Spring boot failure — a stuck context is the usual cause.","Verify the health-probe contract (URL + expected body) in waitUntilHealthy matches the backend's actual actuator/health endpoint.","Increase the 60_000ms budget if the target machines are known-slow, or make the timeout configurable.","Ensure required native dependencies are bundled/installed for the target OS.","After MAX_RESTART_ATTEMPTS the service stops retrying — surface the 'restart the app' alert to the user rather than looping forever."],"exampleFix":"// before\nconst healthy = await this.waitUntilHealthy(60_000);\nif (!healthy) {\n  throw new Error(\"Backend did not become healthy after restart\");\n}\n\n// after — capture the last health-probe failure for diagnostics\nconst probe = await this.waitUntilHealthy(60_000);\nif (!probe.healthy) {\n  throw new BackendUnhealthyError({ lastStatus: probe.lastStatus, lastBody: probe.lastBody });\n}","handlingStrategy":"retry","validationCode":"import { tauriBackendService } from \"@app/desktop/services/tauriBackendService\";\n\nfunction canRecover(): boolean {\n  // recovery continues only while attempts remain\n  return tauriBackendService.restartAttempts < 3; // MAX_RESTART_ATTEMPTS\n}","typeGuard":"function isBackendUnhealthy(e: unknown): boolean {\n  return e instanceof Error && /did not become healthy/.test(e.message);\n}","tryCatchPattern":"// The service already retries up to MAX_RESTART_ATTEMPTS internally.\n// At the app layer, react to the terminal 'unhealthy' status rather than catching here:\ntauriBackendService.subscribeToStatus((s) => {\n  if (s === 'unhealthy') showRestartAppPrompt();\n});","preventionTips":["Bundle required native deps (LibreOffice/PDFTk) for the target OS.","Keep the health-probe contract in waitUntilHealthy in sync with the backend's health endpoint.","Surface sidecar logs during recovery for faster diagnosis.","After max attempts, prompt a full app restart."],"tags":["desktop","backend-service","health-check","recovery","startup"],"backgroundTag":null,"analyzedSha":"9ef20dcab80b85041912f045e17a6aea1d08f969","analyzedAt":"2026-08-13T22:11:39.827Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}