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

  1. Check the sync-server logs for 'Error on endpoint <url>' and read the stacktrace — that is the real cause of the 500.
  2. Verify server config/env (account db path, userFiles dir, secrets) and that the sqlite database file is intact and writable.
  3. Retry the request after fixing the server-side cause; if persistent, restore the database from backup.
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/f56c80fed9e45242. Report an issue: GitHub.