Repository navigation
Move off the driver that Node 24.19 aborts - #160
Merged
Merged
Conversation
Node 24.19.0 added self-removing cleanup hooks to node::ObjectWrap (nodejs/node#63642). better-sqlite3 v11 uses the NAN-style ObjectWrap, whose statement destructor calls RemoveEnvironmentCleanupHook under GC with no entered context, so the process aborts: "Assertion failed: (env) != nullptr" at hooks.cc:142, SIGABRT. It fires on garbage collection, not on any request, which is why the bench crashes looked intermittent and unrelatable to what the operator was doing. The abort is not fail-safe - a powered loco keeps running until systemd restarts the unit ~5 s later. v13's node-addon-api rewrite removes the path entirely: the prebuilt linux-x64.node imports zero cleanup-hook symbols (60 napi symbols, none of AddEnvironmentCleanupHook/RemoveEnvironmentCleanupHook), confirmed against the binary on the bench. v13 also ships prebuilds that load on any Node >=22, so it no longer compiles on the box. Verified end to end: full suite green (1427 backend + 273 frontend), lint and tsc clean, drizzle-kit migrate applies all 15 migrations, the SqliteError codes the auth guard reads (SQLITE_CONSTRAINT_UNIQUE, SQLITE_CONSTRAINT_TRIGGER) survive, VACUUM INTO / readonly / fileMustExist still work, and the live Westgate Hollow DB passes integrity_check and foreign_key_check opened read-only under v13 on the bench. Bumps @fastify/static 8.3.0 -> 10.1.3 in the same PR, clearing a high advisory (route-guard bypass via encoded path separators / non-canonical paths) that lands on the deployment.md D3 SPA auth exemption. v10's only breaking change is setHeaders(reply); we don't use setHeaders. staticFrontend.test.ts covers the exemption path, traversal negative included, and stays green. engines.node raised 20 -> 22 (v13's floor). Docs moved with the code: deployment.md gains the pin rationale, current-state.md the long form, bootstrap.sh the corrected native-module note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bench box crashed and restarted repeatedly on 2026-08-23 with no common operator action behind it. The stack trace pointed straight at the driver:
What was aborting
Node 24.19.0 landed on the bench that morning (NodeSource, 4 minutes before the service first started) and added self-removing cleanup hooks to
node::ObjectWrap(nodejs/node#63642). better-sqlite3 v11 derivesStatement/Database/etc. from the NAN-styleObjectWrap, whose destructor callsRemoveEnvironmentCleanupHook. That runs from a V8 GC weak callback with no entered context, soEnvironment::GetCurrent(isolate)is null and theCHECK_NOT_NULLaborts.The trigger is the garbage collector, not any request — which is exactly why it looked intermittent and unrelatable. drizzle prepares and discards statements per query, so candidates are produced continuously; the timing is a lottery. 8 aborts in the journal, all on 24.19.0; the dev machine on 24.15.0 never saw it.
It is also not fail-safe: a loco under power keeps running until systemd restarts the unit ~5 s later.
The fix
better-sqlite3
^11→^13.0.3. v13's node-addon-api rewrite removes the crashing path entirely — this is structural, not probabilistic. Comparing the two prebuilt binaries on the bench:v13 also ships N-API prebuilds for every platform, so nothing compiles on the box any more, and the ABI coupling that caused this is gone permanently — a floating
24.xstays safe.Between v11.10.0 and v13.0.3 the only breaking change was v12.0.0 dropping EOL Node 18; v13 is additive API only. SQLite 3.49.2 → 3.53.4.
@fastify/staticin the same PR (security)npm auditflagged a high on@fastify/static8.3.0 — four advisories including route-guard bypass via encoded path separators and authorization bypass via non-canonical URL paths. That lands squarely ondeployment.mdD3, the deny-by-default predicate exempting the SPA from the auth hook. Bumped^8.3.0→^10.1.3, which clears it. v10's only breaking change issetHeaders(reply)instead ofres; we don't usesetHeaders. The remaining 4 moderate advisories are the documented, unreachable dev-only esbuild/drizzle-kit chain (--forcestays banned).Verified
npm testfrom root: 1427 backend + 273 frontend passing, exit 0npm run lint+tsc: cleanstaticFrontend.test.ts(the SPA auth-exemption path, incl. a traversal negative and the deny-by-default 401): green against v10.1.3drizzle-kit migrate: all 15 migrations apply to a fresh DBSqliteError.codepreserved (SQLITE_CONSTRAINT_UNIQUE,SQLITE_CONSTRAINT_TRIGGER) — the last-admin guard and username-taken translation inauthRepository.tsstill workVACUUM INTO/readonly/fileMustExist: work (deploy/backup-db.cjsunaffected)integrity_checkok,foreign_key_checkempty, WAL intact, all 13 tables and 15 migrations present, opened read-onlyDocs moved with the code
engines.node>=20→>=22(v13's floor)docs/deployment.md— the pin rationale: why the driver floor is>=13and why not to pin Node back to dodge a builddocs/current-state.md— the long form of that in the Deployment rowdeploy/bootstrap.sh— corrected native-module note (better-sqlite3 no longer compiles on the box; serialport and argon2 still can)🤖 Generated with Claude Code