mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
An update/restart could crash the whole server at boot with TypeError: Cannot create property 'message' on string 'Database closed', masking the real failure. driverFactory.ts's preInitSqlJs() cached its sql.js WASM adapter per file path but never checked whether it had since been closed by a racing gracefulShutdown/resetDbInstance; reusing the dead handle made the next query throw sql.js's own raw string "Database closed" straight out of instrumentation-node.ts's previously-unguarded ensureDbInitialized() call. Next.js's registerInstrumentation() wrapper unconditionally does err.message = ... on whatever register() rejects with, and assigning .message on a primitive string throws in strict mode -- that secondary TypeError is what actually crashed the process. Fixed in two parts: preInitSqlJs() now evicts a closed cached adapter instead of returning it, and a new ensureDbReadyForBoot() normalizes any non-Error throw and retries once for a transient "database closed" message before re-throwing anything else as a real Error.
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
normalizeBootError,
|
|
ensureDbReadyForBoot,
|
|
} from "../../src/instrumentation-node";
|
|
|
|
// Regression guard for #6560: during an update/restart, sql.js's WASM adapter
|
|
// can throw the bare string `"Database closed"` (not an Error instance) when a
|
|
// stale, already-closed handle is reused. Next.js's own
|
|
// `registerInstrumentation()` wrapper unconditionally does
|
|
// `err.message = ...` on whatever `register()` rejects with — if that value is
|
|
// a primitive string, the assignment itself throws
|
|
// `TypeError: Cannot create property 'message' on string 'Database closed'`,
|
|
// masking the real error and crashing the whole server on every boot.
|
|
|
|
test("normalizeBootError wraps a raw thrown string into a real Error", () => {
|
|
const normalized = normalizeBootError("Database closed");
|
|
assert.ok(normalized instanceof Error, "must be a real Error instance");
|
|
assert.equal(normalized.message, "Database closed");
|
|
|
|
// The exact crash from #6560: assigning `.message` on the original raw
|
|
// string throws; on the normalized Error it must be a no-op assignment.
|
|
assert.throws(() => {
|
|
// @ts-expect-error - deliberately mutating a primitive to reproduce the bug
|
|
("Database closed").message = "mutated";
|
|
}, TypeError);
|
|
assert.doesNotThrow(() => {
|
|
normalized.message = `An error occurred while loading instrumentation hook: ${normalized.message}`;
|
|
});
|
|
assert.equal(
|
|
normalized.message,
|
|
"An error occurred while loading instrumentation hook: Database closed"
|
|
);
|
|
});
|
|
|
|
test("normalizeBootError passes real Error instances through unchanged (identity)", () => {
|
|
const original = new Error("boom");
|
|
const normalized = normalizeBootError(original);
|
|
assert.equal(normalized, original);
|
|
});
|
|
|
|
test("normalizeBootError wraps non-string, non-Error throws (null/undefined/object)", () => {
|
|
assert.equal(normalizeBootError(null).message, "null");
|
|
assert.equal(normalizeBootError(undefined).message, "undefined");
|
|
assert.equal(normalizeBootError({ code: "X" }).message, "[object Object]");
|
|
});
|
|
|
|
test("ensureDbReadyForBoot retries once and succeeds when the first attempt throws the raw 'Database closed' string (RED before fix, GREEN after)", async () => {
|
|
let calls = 0;
|
|
const fakeEnsureDbInitialized = async (): Promise<void> => {
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
throw "Database closed";
|
|
}
|
|
// second attempt (post-retry) succeeds, as it would once the caller gets a
|
|
// fresh, non-stale adapter.
|
|
};
|
|
|
|
await assert.doesNotReject(ensureDbReadyForBoot(fakeEnsureDbInitialized));
|
|
assert.equal(calls, 2, "must retry exactly once after a transient 'Database closed' failure");
|
|
});
|
|
|
|
test("ensureDbReadyForBoot re-throws (normalized) when the retry also fails", async () => {
|
|
let calls = 0;
|
|
const fakeEnsureDbInitialized = async (): Promise<void> => {
|
|
calls += 1;
|
|
throw "Database closed";
|
|
};
|
|
|
|
await assert.rejects(
|
|
ensureDbReadyForBoot(fakeEnsureDbInitialized),
|
|
(err: unknown) => {
|
|
assert.ok(err instanceof Error, "rejection must be a real Error, never a raw string");
|
|
assert.equal((err as Error).message, "Database closed");
|
|
return true;
|
|
}
|
|
);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test("ensureDbReadyForBoot does not retry and re-throws (normalized) unrelated failures", async () => {
|
|
let calls = 0;
|
|
const fakeEnsureDbInitialized = async (): Promise<void> => {
|
|
calls += 1;
|
|
throw new Error("disk full");
|
|
};
|
|
|
|
await assert.rejects(
|
|
ensureDbReadyForBoot(fakeEnsureDbInitialized),
|
|
(err: unknown) => {
|
|
assert.ok(err instanceof Error);
|
|
assert.equal((err as Error).message, "disk full");
|
|
return true;
|
|
}
|
|
);
|
|
assert.equal(calls, 1, "unrelated errors must not trigger the closed-DB retry");
|
|
});
|
|
|
|
test("ensureDbReadyForBoot resolves without retrying when there is no failure", async () => {
|
|
let calls = 0;
|
|
const fakeEnsureDbInitialized = async (): Promise<void> => {
|
|
calls += 1;
|
|
};
|
|
|
|
await assert.doesNotReject(ensureDbReadyForBoot(fakeEnsureDbInitialized));
|
|
assert.equal(calls, 1);
|
|
});
|