fix(error-handling): surface silent errors across the codebase (#326)

Add console.warn/console.error logging to 22 silent catch blocks across
10 files. Errors in cleanup, migrations, SSO, fleet snapshots, shutdown,
and validation are now visible in logs. ENOENT guards added to
file-system catches to distinguish missing files from permission errors.
No control flow changes.
This commit is contained in:
Anso
2026-04-01 21:56:41 -04:00
committed by GitHub
parent eb9921e678
commit 10597d213a
11 changed files with 97 additions and 38 deletions
+35 -15
View File
@@ -517,7 +517,8 @@ app.get('/api/auth/sso/providers', (_req: Request, res: Response): void => {
try {
const providers = SSOService.getInstance().getEnabledProviders();
res.json(providers);
} catch {
} catch (e) {
console.warn('[SSO] Failed to list enabled providers, returning empty list:', (e as Error).message);
res.json([]);
}
});
@@ -621,7 +622,8 @@ app.get('/api/auth/sso/oidc/:provider/callback', async (req: Request, res: Respo
let statePayload: { state: string; codeVerifier: string; provider: string };
try {
statePayload = JSON.parse(cryptoSvc.decrypt(stateCookie));
} catch {
} catch (e) {
console.error('[SSO] Failed to decrypt SSO state cookie:', (e as Error).message);
res.redirect('/?sso_error=Invalid+SSO+session');
return;
}
@@ -1295,8 +1297,8 @@ async function captureLocalNodeFiles(node: Node): Promise<SnapshotNodeData> {
try {
const composeContent = await fsService.getStackContent(stackName);
files.push({ filename: 'compose.yaml', content: composeContent });
} catch {
// Stack has no compose file - skip
} catch (e) {
console.warn(`[Fleet Snapshot] Could not read compose file for stack "${stackName}", skipping:`, (e as Error).message);
continue;
}
try {
@@ -1339,7 +1341,8 @@ async function captureRemoteNodeFiles(node: Node): Promise<SnapshotNodeData> {
const content = await composeRes.text();
files.push({ filename: 'compose.yaml', content });
}
} catch {
} catch (e) {
console.warn(`[Fleet Snapshot] Failed to fetch remote compose for stack "${stackName}":`, (e as Error).message);
continue;
}
try {
@@ -1536,8 +1539,9 @@ app.post('/api/fleet/snapshots/:id/restore', async (req: Request, res: Response)
// Backup current files before restore
try {
await fsService.backupStackFiles(stackName);
} catch {
// Stack may not exist yet - that's ok
} catch (e) {
// Stack may not exist yet before first restore — that's ok
console.warn(`[Fleet Snapshot] Pre-restore backup failed for stack "${stackName}" (may not exist yet):`, (e as Error).message);
}
for (const file of files) {
@@ -2617,7 +2621,11 @@ app.get('/api/stacks/:stackName/env', async (req: Request, res: Response) => {
try {
await fsService.access(envPath);
} catch {
} catch (e: unknown) {
const code = (e as NodeJS.ErrnoException)?.code;
if (code !== 'ENOENT') {
console.error('[Sencho] Unexpected error checking env file existence:', (e as Error).message);
}
return res.status(404).json({ error: 'Env file not found' });
}
@@ -3830,7 +3838,8 @@ app.post('/api/scheduled-tasks', (req: Request, res: Response): void => {
}
}
// Validate cron expression
try { CronExpressionParser.parse(cron_expression); } catch {
try { CronExpressionParser.parse(cron_expression); } catch (e) {
console.warn('[Scheduler] Invalid cron expression rejected:', cron_expression, (e as Error).message);
res.status(400).json({ error: 'Invalid cron expression.' }); return;
}
@@ -3945,7 +3954,8 @@ app.put('/api/scheduled-tasks/:id', (req: Request, res: Response): void => {
}
if (cron_expression) {
try { CronExpressionParser.parse(cron_expression); } catch {
try { CronExpressionParser.parse(cron_expression); } catch (e) {
console.warn('[Scheduler] Invalid cron expression rejected:', cron_expression, (e as Error).message);
res.status(400).json({ error: 'Invalid cron expression.' }); return;
}
}
@@ -4733,11 +4743,21 @@ const gracefulShutdown = (signal: string) => {
server.close(() => {
console.log('[Shutdown] HTTP server closed');
try { LicenseService.getInstance().destroy(); } catch { /* already stopped */ }
try { MonitorService.getInstance().stop(); } catch { /* already stopped */ }
try { ImageUpdateService.getInstance().stop(); } catch { /* already stopped */ }
try { SchedulerService.getInstance().stop(); } catch { /* already stopped */ }
try { DatabaseService.getInstance().getDb().close(); } catch { /* already closed */ }
try { LicenseService.getInstance().destroy(); } catch (e) {
console.warn('[Shutdown] LicenseService cleanup failed:', (e as Error).message);
}
try { MonitorService.getInstance().stop(); } catch (e) {
console.warn('[Shutdown] MonitorService cleanup failed:', (e as Error).message);
}
try { ImageUpdateService.getInstance().stop(); } catch (e) {
console.warn('[Shutdown] ImageUpdateService cleanup failed:', (e as Error).message);
}
try { SchedulerService.getInstance().stop(); } catch (e) {
console.warn('[Shutdown] SchedulerService cleanup failed:', (e as Error).message);
}
try { DatabaseService.getInstance().getDb().close(); } catch (e) {
console.warn('[Shutdown] Database close failed:', (e as Error).message);
}
console.log('[Shutdown] Done - exiting');
process.exit(0);
});
+4 -1
View File
@@ -99,7 +99,10 @@ export class ComposeService {
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
});
} finally {
try { fs.unlinkSync(configPath); fs.rmdirSync(tmpDir); } catch { /* best-effort cleanup */ }
try { fs.unlinkSync(configPath); fs.rmdirSync(tmpDir); } catch (e) {
// Best-effort cleanup: temp config dir may already be removed or locked
console.warn('[ComposeService] Could not clean up temp Docker config dir:', (e as Error).message);
}
}
}
+2 -1
View File
@@ -24,8 +24,9 @@ export class CryptoService {
console.warn(`[CryptoService] Fixing permissive key file permissions (was 0o${mode.toString(8)}, set to 0o600)`);
fs.chmodSync(keyPath, 0o600);
}
} catch {
} catch (e) {
// chmod not supported on this platform (e.g. Windows) — skip
console.warn('[CryptoService] Could not enforce key file permissions (platform may not support chmod):', (e as Error).message);
}
} else {
this.key = crypto.randomBytes(KEY_LENGTH);
+15 -3
View File
@@ -436,7 +436,12 @@ export class DatabaseService {
// Drop legacy SSH/TLS columns from pre-0.7 databases (no longer read or written)
const legacyCols = ['host', 'port', 'ssh_port', 'ssh_user', 'ssh_password', 'ssh_key', 'tls_ca', 'tls_cert', 'tls_key'];
for (const col of legacyCols) {
try { this.db.prepare(`ALTER TABLE nodes DROP COLUMN ${col}`).run(); } catch { /* already dropped or never existed */ }
try { this.db.prepare(`ALTER TABLE nodes DROP COLUMN ${col}`).run(); } catch (e: unknown) {
// Expected: column already dropped or never existed
if (!String((e as Error)?.message).includes('no such column')) {
console.warn(`[DatabaseService] Unexpected error dropping legacy column "${col}":`, (e as Error).message);
}
}
}
// Initialize default global settings if they don't exist
@@ -511,7 +516,12 @@ export class DatabaseService {
private migrateSSOColumns(): void {
const maybeAddCol = (table: string, col: string, def: string) => {
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`).run(); } catch { /* already exists */ }
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`).run(); } catch (e: unknown) {
// Expected: column already exists
if (!String((e as Error)?.message).includes('duplicate column')) {
console.warn(`[DatabaseService] Unexpected error adding column "${col}" to "${table}":`, (e as Error).message);
}
}
};
maybeAddCol('users', 'auth_provider', "TEXT NOT NULL DEFAULT 'local'");
maybeAddCol('users', 'provider_id', 'TEXT DEFAULT NULL');
@@ -562,7 +572,9 @@ export class DatabaseService {
`);
try {
this.db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_role_assignments_unique ON role_assignments(user_id, role, resource_type, resource_id)');
} catch { /* already exists */ }
} catch (e) {
console.warn('[DatabaseService] Could not create role_assignments unique index:', (e as Error).message);
}
}
// --- Agents ---
+13 -7
View File
@@ -651,7 +651,10 @@ class DockerController {
// Destroy the Docker stats stream when the WebSocket closes to prevent
// orphaned streams polling the daemon after client disconnect.
ws.on('close', () => {
try { (stats as any).destroy(); } catch { /* stream already ended */ }
try { (stats as any).destroy(); } catch (e) {
// Stream already ended before client disconnected
console.warn('[DockerController] Stats stream already ended on WS close:', (e as Error).message);
}
});
}
@@ -723,8 +726,9 @@ class DockerController {
case 'resize':
if (msg.rows && msg.cols) {
exec.resize({ h: msg.rows, w: msg.cols }).catch(() => {
// Ignore resize errors (exec may have ended)
exec.resize({ h: msg.rows, w: msg.cols }).catch((e: Error) => {
// Exec may have ended before resize completes
console.warn('[DockerController] Exec resize failed (exec may have ended):', e.message);
});
}
break;
@@ -733,8 +737,9 @@ class DockerController {
// Keep-alive, no-op
break;
}
} catch {
// Non-JSON or malformed message - ignore
} catch (e) {
// Non-JSON or malformed WebSocket message
console.warn('[DockerController] Ignoring malformed exec WS message:', (e as Error).message);
}
});
@@ -742,8 +747,9 @@ class DockerController {
ws.on('close', () => {
try {
stream.destroy();
} catch {
// Ignore destroy errors
} catch (e) {
// Stream already destroyed before WS close
console.warn('[DockerController] Exec stream already destroyed on WS close:', (e as Error).message);
}
});
+17 -7
View File
@@ -275,8 +275,11 @@ export class FileSystemService {
try {
await fsPromises.access(oldEnvPath);
await fsPromises.rename(oldEnvPath, newEnvPath);
} catch {
// No env file to migrate
} catch (e: unknown) {
const code = (e as NodeJS.ErrnoException)?.code;
if (code !== 'ENOENT') {
console.warn(`[FileSystemService] Could not migrate env file for ${stackName}:`, (e as Error).message);
}
}
}
@@ -300,8 +303,11 @@ export class FileSystemService {
try {
await fsPromises.access(src);
await fsPromises.copyFile(src, path.join(backupDir, file));
} catch {
// File doesn't exist, skip
} catch (e: unknown) {
const code = (e as NodeJS.ErrnoException)?.code;
if (code !== 'ENOENT') {
console.warn(`[FileSystemService] Could not back up ${file}:`, (e as Error).message);
}
}
}
@@ -310,8 +316,11 @@ export class FileSystemService {
try {
await fsPromises.access(envSrc);
await fsPromises.copyFile(envSrc, path.join(backupDir, '.env'));
} catch {
// No .env to backup
} catch (e: unknown) {
const code = (e as NodeJS.ErrnoException)?.code;
if (code !== 'ENOENT') {
console.warn('[FileSystemService] Could not back up .env:', (e as Error).message);
}
}
// Write timestamp marker
@@ -343,7 +352,8 @@ export class FileSystemService {
try {
const ts = await fsPromises.readFile(tsFile, 'utf-8');
return { exists: true, timestamp: parseInt(ts, 10) || null };
} catch {
} catch (e) {
console.warn('[FileSystemService] Backup timestamp file unreadable:', (e as Error).message);
return { exists: true, timestamp: null };
}
} catch {
+2 -1
View File
@@ -7,7 +7,8 @@ function getUnixShell() {
try {
execSync('which bash', { stdio: 'ignore' });
return 'bash';
} catch {
} catch (e) {
console.warn('[HostTerminalService] bash not found, falling back to sh:', (e as Error).message);
return 'sh';
}
}
+3 -1
View File
@@ -180,7 +180,9 @@ export class MonitorService {
totalReclaimableBytes += bytes;
}
}
} catch (e) { }
} catch (e) {
console.warn('[MonitorService] Failed to parse Docker system df output:', e);
}
}
const reclaimGb = totalReclaimableBytes / (1024 * 1024 * 1024);
+2 -1
View File
@@ -18,7 +18,8 @@ export function isValidRemoteUrl(
let url: URL;
try {
url = new URL(raw);
} catch {
} catch (e) {
console.warn('[Validation] URL parse failure:', (e as Error).message, '— input:', raw);
return {
valid: false,
reason: 'API URL must be a valid URL (e.g. https://my-server.example.com:3000)',