mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
Compare commits
1 Commits
fix/14021-
...
fix/14060-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7064cca334 |
1
changelog.d/fixes/14060-malformed-sqlite-500.md
Normal file
1
changelog.d/fixes/14060-malformed-sqlite-500.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(db): degrade getSettings() to defaults instead of crashing the Home dashboard when the key_value table is corrupted (#14060)
|
||||
@@ -13,7 +13,17 @@ export const dynamic = "force-dynamic";
|
||||
export default async function HomePage() {
|
||||
// Even if getSettings() rejects, getMachineId() runs concurrently, which is acceptable
|
||||
// as both paths fail-fast on error and avoids the waterfall penalty.
|
||||
const [settings, machineId] = await Promise.all([getSettings(), getMachineId()]);
|
||||
// Defense-in-depth (#14060): getSettings() already degrades to defaults on a corrupted
|
||||
// key_value table, but a future unguarded read anywhere in its dependency chain should
|
||||
// not be able to crash this Server Component render again.
|
||||
const [settings, machineId] = await Promise.all([
|
||||
getSettings().catch((error: unknown) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[Home] Failed to load settings; using defaults: ${message}`);
|
||||
return { setupComplete: false };
|
||||
}),
|
||||
getMachineId(),
|
||||
]);
|
||||
const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true";
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -151,7 +151,13 @@ function applySessionAffinityLegacyFallback(settings: Record<string, unknown>):
|
||||
|
||||
export async function getSettings() {
|
||||
const db = getDbInstance();
|
||||
const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all();
|
||||
let rows: unknown[] = [];
|
||||
try {
|
||||
rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = 'settings'").all();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[DB] Failed to read settings; using defaults: ${message}`);
|
||||
}
|
||||
const settings: Record<string, unknown> = {
|
||||
cloudEnabled: true,
|
||||
tailscaleEnabled: false,
|
||||
|
||||
64
tests/unit/settings-14060-getsettings-corrupt-db.test.ts
Normal file
64
tests/unit/settings-14060-getsettings-corrupt-db.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-issue-14060-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
});
|
||||
|
||||
test("getSettings() degrades to defaults (does not throw) when only the key_value table's page is corrupted", async () => {
|
||||
const db = core.getDbInstance();
|
||||
const dbPath = (db as { name?: string }).name;
|
||||
assert.ok(typeof dbPath === "string" && dbPath.length > 0, "expected a file-backed db path");
|
||||
|
||||
db.prepare(
|
||||
"INSERT INTO key_value (namespace, key, value) VALUES ('settings', 'requireLogin', 'true')"
|
||||
).run();
|
||||
|
||||
const pageSize = (db.pragma("page_size") as Array<{ page_size: number }>)[0].page_size;
|
||||
const rootPageRow = db
|
||||
.prepare("SELECT rootpage FROM sqlite_master WHERE type = 'table' AND name = 'key_value'")
|
||||
.get() as { rootpage: number } | undefined;
|
||||
assert.ok(rootPageRow?.rootpage, "expected to find key_value's rootpage in sqlite_master");
|
||||
const rootPage = rootPageRow!.rootpage;
|
||||
|
||||
core.resetDbInstance();
|
||||
|
||||
const buf = fs.readFileSync(dbPath as string);
|
||||
const pageStart = (rootPage - 1) * pageSize;
|
||||
for (let i = pageStart; i < Math.min(buf.length, pageStart + pageSize); i++) {
|
||||
buf[i] = 0xff;
|
||||
}
|
||||
fs.writeFileSync(dbPath as string, buf);
|
||||
|
||||
const reopened = core.getDbInstance();
|
||||
const bootHealthy = !!reopened
|
||||
.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'provider_connections'"
|
||||
)
|
||||
.get();
|
||||
assert.equal(bootHealthy, true, "expected the boot probe's own tables to remain readable");
|
||||
core.resetDbInstance();
|
||||
|
||||
const settings = await settingsDb.getSettings();
|
||||
|
||||
assert.equal(
|
||||
typeof settings,
|
||||
"object",
|
||||
"expected getSettings() to degrade to a defaults object instead of throwing"
|
||||
);
|
||||
assert.equal(
|
||||
settings.requireLogin,
|
||||
true,
|
||||
"expected the built-in default for requireLogin (row could not be read from the corrupted table)"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user