Manifest parse failure panics hero_router on startup (bad fallback) #48
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_router#48
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Category: error-handling / high
What
If
~/.hero/router/manual_sources.jsonexists but fails to deserialize,hero_routerpanics and refuses to start.Where
crates/hero_router/src/main.rs:297-301Why it's wrong
Manifest::load(seemanifest.rs:42-54) reads the file withread_to_stringthen passes the string toserde_json::from_str. The intended fallback —Manifest::load("/dev/null")— does not short-circuit:/dev/nullexists, soloadreads it (yielding""), then callsserde_json::from_str(""), which fails with "EOF while parsing a value". The outerunwrap_or_elsethen panics, taking the whole router down.Net effect: any corruption, truncation, or schema drift in
manual_sources.jsoncrashes the router at startup, with a useless message ("failed to load manifest", no context about which path or what parse error).Reproduction
Suggested fix
Fall back to an in-memory empty manifest pointing at the original path, so the next
save()writes a valid file:This requires making
Manifest { path, sources }constructable — either expose anempty(path: PathBuf) -> Selfconstructor, or make the fieldspub(they already are forsources; addpathtoo, or add the constructor).Additional: log the load error
At minimum, even if the fallback is kept, log the original error instead of discarding it — today the
|_|arm swallows the parse error entirely, so a user can't see why their manifest didn't load.Implementation Spec for Issue #48
Objective
Make
hero_routertolerate a malformedmanual_sources.jsonat startup by logging the parse error and falling back to an empty in-memory manifest that still points at the original path.Requirements
manual_sources.jsonmust not crash the router on startup.tracing::warn!with structuredpathanderrorfields.Manifestmust retain the originalcfg.manifest_pathso that the nextsave()overwrites the bad file with valid JSON.Files to Modify/Create
crates/hero_router/src/manifest.rs— add a newManifest::empty(path: PathBuf) -> Selfassociated constructor, plus unit tests.crates/hero_router/src/main.rs— replace the broken/dev/nullfallback at lines 297-301 with amatchthat logs and falls back toManifest::empty.Implementation Plan
Step 1: Add
Manifest::emptyconstructorFiles:
crates/hero_router/src/manifest.rsimpl Manifest, add:pathfield; keep encapsulation.Dependencies: none
Step 2: Replace the panicking fallback at the call site
Files:
crates/hero_router/src/main.rsanyhow::Errorreturned byManifest::loadalready carries the full context chain, so%erris sufficient.tracingis already a workspace dependency and already in scope inmain.rs— no new imports needed.Dependencies: Step 1
Step 3: Tests
Files:
crates/hero_router/src/manifest.rs(append inside the existing#[cfg(test)] mod tests)Manifest::emptyproduces a usable, empty manifest whosesave()round-trips viaload.loadfails on malformed JSON, so a future refactor cannot silently swallow parse errors.Dependencies: Step 1
Acceptance Criteria
hero_routerstarts successfully even ifmanual_sources.jsoncontains invalid JSON.tracing::warn!withpathanderrorfields.cfg.manifest_path(so the nextadd/removepersists correctly).cargo test --workspacepasses, including the two new tests.cargo clippy --workspace --all-targets -- -D warningspasses.Notes
development.pathfield ofManifeststays private; theempty(path)constructor keeps encapsulation.Cargo.tomlchanges needed —tracingandanyhowalready declared.add/removesubcommands keep their current error-propagating behavior — a user runninghero_router add ...against a corrupt file should see the parse error, not silently overwrite it.Scope update
While implementing the approved spec, the same class of broken fallback was found at two additional sites in
main.rs:cmd_list(was lines 552-553):Manifest::load(&cfg.manifest_path).unwrap_or_else(|_| Manifest::load_default().unwrap_or_else(|_| panic!("manifest error")))cmd_scan(was lines 601-602): same pattern.The inner
Manifest::load_default()loads~/.hero/router/manual_sources.json, which is the same filecfg.manifest_pathpoints at for default installs — so a corrupt manifest makeshero_router listandhero_router scanpanic too.Since both are read-only commands, the fix is identical to the startup path. To avoid three near-identical match blocks, the implementation was refactored to a single module-level helper:
All three call sites (
cmd_start,cmd_list,cmd_scan) now go throughload_manifest_or_empty.The mutating CLI commands (
cmd_add,cmd_remove) retain their current?-propagating behavior — a user running those against a corrupt file should see the parse error, not silently overwrite it.Updated acceptance criteria (added)
hero_router listsucceeds on a corruptmanual_sources.json.hero_router scansucceeds on a corruptmanual_sources.json.hero_router add/hero_router removestill fail loudly on a corrupt file (no behavior change).Test Results
Lint
cargo clippy --workspace --all-targets -- -D warnings: passcargo fmt --all --check: passNew tests added in this PR
manifest::tests::empty_then_save_writes_valid_file_at_given_pathmanifest::tests::load_fails_on_malformed_jsonImplementation Summary
Closing #48 via PR (to be linked in a follow-up comment).
Changes
crates/hero_router/src/manifest.rsManifest::empty(path: PathBuf) -> Self— constructs an empty manifest in-memory, bound to the given path, without touching disk.#[derive(Debug)]onManifest(required by the newload_fails_on_malformed_jsonunit test'sunwrap_err()).crates/hero_router/src/main.rsload_manifest_or_empty(path: &Path) -> Manifestat module scope abovecmd_list. On parse/read failure it logs viatracing::warn!with structuredpathanderrorfields, then returnsManifest::empty(path.to_path_buf()).unwrap_or_else(|_| panic!(...))fallbacks with a single call to the helper:build_and_start(startup path) — was lines 297-301 using a/dev/nullfallback that itself panicked.cmd_list— was lines 552-555 using aload_default()fallback that pointed at the same file.cmd_scan— was lines 601-604 using the same pattern ascmd_list.The mutating CLI commands (
cmd_add,cmd_remove) retain their?-propagating behavior — a user running those against a corrupt manifest should see the parse error, not silently overwrite it.Why not
/dev/nullas the fallback?Manifest::loadpassespath.exists(), reads"", andserde_json::from_str("")fails — the fallback itself fails, causing the outer panic.Manifest.pathwould be/dev/null, so any latersave()would silently discard user data.Manifest::empty(path)avoids disk I/O entirely and preserves the correct persistence target, so the nextsave()overwrites the bad file.Tests added
manifest::tests::empty_then_save_writes_valid_file_at_given_path— confirmsManifest::emptyroundtrips throughadd()+save()+Manifest::load.manifest::tests::load_fails_on_malformed_json— documents the failure mode so a future refactor cannot silently swallow parse errors.End-to-end reproduction
Ran the exact scenario from the issue body against the fixed binary:
Test results
cargo test --workspace— 73 passed / 0 failed / 0 ignored.cargo clippy --workspace --all-targets -- -D warnings— clean.cargo fmt --all --check— clean.Pull request opened: #53
This PR implements the changes discussed in this issue.