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.
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)
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
- Sign up failed
- Failed to save authentication token
- Cannot connect to server. Please check the server URL and en
- Login request timed out. Please check your network connectio
- Cannot resolve server address. Please check the server URL i
AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13).
Data as JSON: /api/errors/25eba75e0d997f03.
Report an issue: GitHub.