actualbudget/actual · critical
internal-error
internal-error
Error message
internal-error
What it means
errorMiddleware is the sync-server's catch-all Express error handler: any unhandled error in a route is logged ('Error on endpoint %s' with url and stacktrace) and converted into a 500 response { status: 'error', reason: 'internal-error' }. The client never sees the real cause — the details are on the server logs only.
Source
Thrown at packages/sync-server/src/util/middlewares.ts:30
) {
if (res.headersSent) {
// If you call next() with an error after you have started writing the response
// (for example, if you encounter an error while streaming the response
// to the client), the Express default error handler closes
// the connection and fails the request.
// So when you add a custom error handler, you must delegate
// to the default Express error handler, when the headers
// have already been sent to the client
// Source: https://expressjs.com/en/guide/error-handling.html
return next(err);
}
console.log(`Error on endpoint %s`, {
requestUrl: req.url,
stacktrace: err.stack,
});
res.status(500).send({ status: 'error', reason: 'internal-error' });
}
const validateSessionMiddleware = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const session = await validateSession(req, res);
if (!session) {
return;
}
res.locals = session;
next();
};
const requestLoggerMiddleware = expressWinston.logger({
transports: [new winston.transports.Console()],View on GitHub (pinned to d4334cb6e6)
Solutions
- Check the sync-server logs for 'Error on endpoint <url>' and read the stacktrace — that is the real cause of the 500.
- Verify server config/env (account db path, userFiles dir, secrets) and that the sqlite database file is intact and writable.
- Retry the request after fixing the server-side cause; if persistent, restore the database from backup.
- If you own the code, wrap route handlers (or rely on errorMiddleware) and add targeted logging for the failing endpoint.
Example fix
// before (server log only)
// Error on endpoint /sync ... stacktrace: SqliteError: database is locked
// after: resolve the cause, e.g. single-writer access or enable WAL
// db.pragma('journal_mode = WAL'); Defensive patterns
Strategy: try-catch
Validate before calling
// client-side: check server health before calling
const health = await fetch(base + '/health');
if (!health.ok) throw new Error('sync server unhealthy'); Try / catch
const res = await callSyncEndpoint();
if (res.status === 500 && (await res.json()).reason === 'internal-error') {
// details are server-side: log request id/url, retry once after backoff, then escalate to server logs
await new Promise(r => setTimeout(r, 1000));
retryOrEscalate();
} Prevention
- Monitor sync-server logs for 'Error on endpoint' lines — that is where the real stacktrace lives.
- Keep the sqlite database on reliable storage and take regular backups.
- Pin well-tested server versions; test env-var changes in staging.
- Add per-endpoint error handling in any custom server code so errors are specific, not generic 500s.
When it happens
Trigger: Any route throwing/rejecting outside its own try/catch: database failures (sqlite corruption/locked), unhandled exceptions in handlers, JSON body parse errors surfacing late, errors in async middleware (e.g. session validation) not caught upstream.
Common situations: Corrupt or locked better-sqlite3 database on self-hosted servers; missing/misconfigured environment variables failing lazily; bugs in custom forks/plugins; transient filesystem errors in endpoints lacking their own error handling.
Related errors
- Failed to fetch catalog: ${response.statusText}
- Failed to fetch CSS from ${url}: ${response.status} ${respon
- Document directory is not set
- getDocumentDir: id is falsy: ${id}
- ${text}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/f56c80fed9e45242.
Report an issue: GitHub.