Stirling-Tools/Stirling-PDF · critical · Error
Backend did not become healthy after restart
Error message
Backend did not become healthy after restart
What it means
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.
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.
Example fix
// before
const healthy = await this.waitUntilHealthy(60_000);
if (!healthy) {
throw new Error("Backend did not become healthy after restart");
}
// after — capture the last health-probe failure for diagnostics
const probe = await this.waitUntilHealthy(60_000);
if (!probe.healthy) {
throw new BackendUnhealthyError({ lastStatus: probe.lastStatus, lastBody: probe.lastBody });
} Defensive patterns
Strategy: retry
Validate before calling
import { tauriBackendService } from "@app/desktop/services/tauriBackendService";
function canRecover(): boolean {
// recovery continues only while attempts remain
return tauriBackendService.restartAttempts < 3; // MAX_RESTART_ATTEMPTS
} Type guard
function isBackendUnhealthy(e: unknown): boolean {
return e instanceof Error && /did not become healthy/.test(e.message);
} Try / catch
// The service already retries up to MAX_RESTART_ATTEMPTS internally.
// At the app layer, react to the terminal 'unhealthy' status rather than catching here:
tauriBackendService.subscribeToStatus((s) => {
if (s === 'unhealthy') showRestartAppPrompt();
}); Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Backend URL not available - backend may still be starting
- Cannot connect to server. Please check the server URL and…
- Cannot resolve server address. Please check the server URL…
- Connection mode is locked by provisioning
- Download failed ( )
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/25eba75e0d997f03.
Report an issue: GitHub.
Appendix: source
Thrown at frontend/editor/src/desktop/services/tauriBackendService.ts:133
title: "Backend stopped unexpectedly",
body: `Attempting to restart... (${this.restartAttempts}/${TauriBackendService.MAX_RESTART_ATTEMPTS})`,
durationMs: 5000,
});
this.isRecovering = true;
// Reset started flag so startBackend() will run again
this.backendStarted = false;
this.startPromise = null;
// Fresh grace window: the restarted backend needs boot time before failed
// health checks may count as unhealthy again.
this.hasBeenHealthy = false;
this.setStatus("starting");
try {
await this.startBackend();
// startBackend resolves once the port is known, not once Spring is up -
// only declare success after a real health check passes.
const healthy = await this.waitUntilHealthy(60_000);
if (!healthy) {
throw new Error("Backend did not become healthy after restart");
}
this.restartAttempts = 0; // Reset on successful restart
this.isRecovering = false;
console.log("[TauriBackendService] Backend restarted successfully.");
alert({
alertType: "success",
title: "Backend restarted",
body: "The local backend is back online.",
durationMs: 4000,
});
} catch (err) {
console.error("[TauriBackendService] Restart failed:", err);
// Set isRecovering = false BEFORE setStatus to prevent re-triggering scheduleRecovery
// if the max attempts check above doesn't catch it next time.
this.isRecovering = false;
if (this.restartAttempts < TauriBackendService.MAX_RESTART_ATTEMPTS) {
this.setStatus("unhealthy"); // Will trigger another scheduleRecovery
} else {View on GitHub (pinned to 9ef20dcab8)