mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
fix(server): last-resort uncaughtException/unhandledRejection safety net (#114)
A FK constraint violation crashed the whole process on 1.9.1-beta2 with a bare "FOREIGN KEY constraint failed" and NO stack — so it couldn't be root- caused. better-sqlite3 is synchronous, so such a throw inside a socket.io handler (no local try/catch) propagates to uncaughtException, and with no handler Node exits traceless. Add a small top-of-server.js net that logs the FULL err.stack (file:line of the offending write) + timestamp, best-effort closes the DB (WAL flush), then exits(1) so systemd restarts fresh. NOT catch-and-continue — after an uncaught throw the process state is undefined, so we never keep serving. This is the investigation tool the root-cause fix is blocked on, plus the fleet-wide-crash net #114 asked for. Verified (not assumed): - A synthetic synchronous FK throw inside a real socket.io handler IS caught by uncaughtException, logs the full stack incl. the throwing file:line, exits 1. - Non-over-reach: a FK throw in an Express route -> Express handles it (500), a throw in a local try/catch -> caught (200); the global net does NOT fire and the process stays alive. Last resort, not a catch-all. - 149 server tests green; server boots clean (net doesn't trip on startup). The root-cause FK fix is SEPARATE and waits on the stack trace this produces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7539603b17
commit
78a4ee4d37
|
|
@ -8,6 +8,26 @@ const fs = require('fs');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
const VERSION = require('./version');
|
const VERSION = require('./version');
|
||||||
|
|
||||||
|
// #114: last-resort crash safety net. better-sqlite3 is SYNCHRONOUS, so a constraint
|
||||||
|
// violation (e.g. a FK write) inside a socket.io handler with no local try/catch
|
||||||
|
// propagates to uncaughtException; Node's default then prints a bare message and exits
|
||||||
|
// with NO stack — which is exactly why #114's "FOREIGN KEY constraint failed" couldn't
|
||||||
|
// be root-caused. This handler logs the FULL STACK (the file:line of the offending
|
||||||
|
// write) then exits(1) so systemd restarts a fresh process. It is NOT catch-and-
|
||||||
|
// continue: after an uncaught throw the process state is undefined, so we never keep
|
||||||
|
// serving. Registered before everything else so it's in place during startup too.
|
||||||
|
// (Verified: uncaughtException does catch a synchronous socket.io-handler throw.)
|
||||||
|
function logFatalAndExit(kind, err) {
|
||||||
|
try {
|
||||||
|
const e = err instanceof Error ? err : new Error('Non-error thrown: ' + require('util').inspect(err));
|
||||||
|
process.stderr.write(`\n[FATAL ${kind}] ${new Date().toISOString()}\n${e.stack || e.message}\n`);
|
||||||
|
} catch (_) { /* the death handler must never throw */ }
|
||||||
|
try { require('./db/database').db.close(); } catch (_) { /* best-effort WAL flush */ }
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
process.on('uncaughtException', (err) => logFatalAndExit('uncaughtException', err));
|
||||||
|
process.on('unhandledRejection', (reason) => logFatalAndExit('unhandledRejection', reason));
|
||||||
|
|
||||||
// Ensure upload directories exist
|
// Ensure upload directories exist
|
||||||
[config.contentDir, config.screenshotsDir].forEach(dir => {
|
[config.contentDir, config.screenshotsDir].forEach(dir => {
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue