fix(config): never let a bad config.json brick startup - #34
Closed
SanikaA3 wants to merge 1 commit into
Closed
Conversation
HAXCMS is a module-level singleton, so its constructor runs at require
time. The config load had no guards:
if (!fs.existsSync(path.join(this.configDirectory, "config.json"))) {
fs.copyFileSync(boilerplate, ...);
}
this.config = JSON.parse(fs.readFileSync(...));
Two unguarded calls. A present-but-invalid config.json throws SyntaxError
out of JSON.parse, and a read-only config directory throws EACCES out of
copyFileSync. Either one escapes the constructor at require time, so the
process cannot start at all.
Reproduced against main with a temp _config holding a corrupt and then an
empty config.json - both give:
CRASH: SyntaxError: Unexpected end of JSON input
Extracts the load into src/lib/loadSystemConfig.js, which separates the
three failure modes because they need different responses:
missing restore boilerplate onto disk (best effort; a read-only
directory now falls through instead of throwing)
empty fall back in memory
corrupt fall back in memory and LEAVE THE FILE ALONE
Not overwriting a corrupt-but-present file is deliberate. It may be a
symlink to a shared config carrying hand edits (API keys, themes);
replacing it with boilerplate would destroy them. The log tells the admin
what to fix so they keep their customizations.
The returned config is never null, so downstream property access in the
constructor cannot fatal. Each failure mode logs its real cause,
including the underlying parse or errno message.
One subtlety: the shipped boilerplate sets
deploymentProfile: self-hosted-multi-site. Carrying that into a degraded
boot would enable MCP and relax IAM tenant validation on a single-site or
IAM deployment, so the in-memory fallback drops the key and lets the
existing constructor logic derive the profile from the real operating
context. The minimal last-resort config likewise ships mcp closed
(enabled: false, readOnly: true) so a degraded boot never opens more
surface than a healthy one.
Verification:
- unit suite 939/939 on main, 950/950 here (11 new)
- corrupt and empty configs now boot with a diagnostic instead of
crashing; valid configs are unchanged and log nothing
- a corrupt file is byte-for-byte identical after load
- unwritable config directory returns a usable config and logs
Scope: this is the haxcms-nodejs half of #2967. The PHP loader in
haxcms-php is the issue's primary target and is not touched here.
Refs #2967
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review or Edit in CodeSandboxOpen the branch in Web Editor • VS Code • Insiders |
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Refs #2967. This is the haxcms-nodejs half of that issue — see Scope at the bottom.
Problem
HAXCMSis exported as a module-level singleton, so its constructor runs atrequiretime. The config load had no guards on either of its two throwing calls:JSON.parsethrowsSyntaxErroron a present-but-invalidconfig.jsoncopyFileSyncthrowsEACCESwhen the config directory is read-onlyEither escapes the constructor at require time, so the process does not start at all. The existing comment says "self healing if config is missing" and that part works — the gap is a file that exists but cannot be parsed.
Reproduced against
mainwith a temp_configdirectory:Note that an empty file fails identically to a corrupt one — which is what a truncated write or a full disk leaves behind.
Fix
Extracts the load into
src/lib/loadSystemConfig.js, alongside the existingdiscoverConfigPath.js. It separates the three failure modes, because they want different responses:The call site becomes one line, and the returned config is never null, so downstream property access in the constructor cannot fatal. Every failure mode logs its real cause, including the underlying parse or errno message.
Two decisions worth calling out
A corrupt file is never overwritten. The obvious fix would be to re-copy the boilerplate over bad JSON. This deliberately does not: in HAXiam that path is a symlink to a shared
_iamConfig/config.jsonthat may hold API keys and theme customizations, and replacing it to fix a stray comma would destroy them. The site stays up on in-memory defaults and the log tells the admin what to repair. There is a test asserting the file is byte-for-byte unchanged after a failed load.The in-memory fallback drops
deploymentProfile. The shipped boilerplate sets"deploymentProfile": "self-hosted-multi-site". Carrying that into a degraded boot would stamp a multi-site profile onto a single-site or IAM install, enabling MCP and relaxing IAM tenant validation — the same hazard the issue raises for the PHP loader. Dropping the key lets the constructor's existing context-aware logic derive the profile instead. The last-resort minimal config likewise shipsmcp: { enabled: false, readOnly: true }, so a degraded boot never opens more surface than a healthy one.Testing
The existing unit suite was executed and passes — no regressions:
mainnpm run test:unitThe 11 additional tests are
test/unit/load-system-config.test.cjs, covering: valid load (and that a healthy load logs nothing), missing-file restore, corrupt, empty, non-object JSON, unwritable config directory, unusable boilerplate, thedeploymentProfiledrop,mcpclosed in the minimal config, that a corrupt file is left untouched, and that the returned config is never null or non-object across a range of malformed inputs.Behaviour after the fix, same reproduction as above:
A valid config is unchanged and logs nothing.
Scope
#2967 is only half addressed by this PR. The issue names
haxcms-phpas the primary target (IAM runs the PHP core) and describes safeguards 1, 2 and 3 forsystem/backend/php/lib/HAXCMS.php, with haxcms-nodejs getting parity for the corrupt case. This PR is the nodejs parity half only.I left the PHP side alone rather than guess at it: I could not run PHP to verify the change, and an unverified constructor restructure in a security-adjacent path seemed worse than none. The issue splits cleanly by codebase, so this stands on its own — but #2967 should stay open for the PHP loader.
Happy to take feedback or adjust anything here.
🤖 Generated with Claude Code