Skip to content

client: drop poison panics and surface invite parse errors#139

Merged
intendednull merged 1 commit into
mainfrom
claude/issue-114-115-client-panics
Apr 11, 2026
Merged

client: drop poison panics and surface invite parse errors#139
intendednull merged 1 commit into
mainfrom
claude/issue-114-115-client-panics

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Summary

Two small robustness fixes in willow-client, both pulled from the review under #108:

  • [client] Fix three lock().unwrap() poison vectors on join_links #114 — drop std::sync::Mutex::lock().unwrap() on join_links so a panic while holding the guard no longer poisons the lock and takes down every future caller (including the gossip listener that handles JoinRequest). The Arc<Mutex<Vec<JoinLink>>> now uses parking_lot::Mutex, which is poison-free by construction. parking_lot is added once at the workspace level and pulled into crates/client only.
  • [client] Propagate UUID parse errors during invite join instead of silently regenerating #115accept_invite previously used unwrap_or_else(|_| Uuid::new_v4()) / unwrap_or_else(|_| ChannelId::new()) to silently invent fresh IDs when an invite's server_id or a channel name failed to parse. That split-brained the joiner from every other peer. Introduce ClientError::MalformedInvite(String) (a new thiserror variant — the first typed error in the crate) and propagate parse / create-channel failures with ?, so the caller sees a clear anyhow::Error that downcasts to ClientError::MalformedInvite instead of a phantom server membership.

Scope is strictly crates/client/ plus the two Cargo.toml entries for parking_lot and thiserror. No other crates touched.

Closes #114
Closes #115
Progresses #108

Test plan

  • cargo fmt --check
  • cargo clippy --workspace -- -D warnings — clean, zero warnings
  • cargo test -p willow-client — 68 tests pass including the two new regression tests:
    • join_links_lock_survives_panic_in_holder — spawns a spawn_blocking task that pushes a link, panics while holding the guard, then asserts the main task can still read/write join_links without panicking.
    • malformed_invite_server_id_is_rejected — builds a real invite encrypted for the test client, tampers the embedded server_id to "not-a-uuid", re-encodes, and asserts accept_invite returns ClientError::MalformedInvite mentioning server_id.
  • cargo test -p willow-state -p willow-channel -p willow-network — all upstream dependents still green.
  • cargo check --target wasm32-unknown-unknown for the full just check-wasm cohort (-p willow-identity -p willow-state -p willow-channel -p willow-messaging -p willow-crypto -p willow-transport -p willow-common -p willow-network -p willow-client -p willow-web) — clean, verifying parking_lot builds on WASM.

https://claude.ai/code/session_018TTGhL645aTR4RZWrRBLPS

Copy link
Copy Markdown
Owner Author

Audit note: this PR and #138 both edit crates/client/src/joining.rs in the same ~10-line region (the else branch that constructs a Server from an accepted invite). The overlap is real — whichever lands second needs a manual rebase.

Planned merge order: #138 first, then rebase this PR on top of main. The rebase will replace the let mut server = Server::new(...); server.id = ServerId(parsed_server_uuid); pair with a single Server::with_id(ServerId(parsed_server_uuid), &accepted.server_name, accepted.genesis_author) call — the upfront UUID validation and the create_channel map_err from this PR both stay.

Also noting for a follow-up (out of scope here): crates/client/src/lib.rs still has a Uuid::parse_str(...).unwrap_or_else(|_| Uuid::new_v4()) pattern in the channel-id reconciliation path. Different context (reading stored IDs rather than untrusted invite payloads), but the same split-brain shape as #115. Tracked in a follow-up issue.


Generated by Claude Code

Fixes two robustness footguns in willow-client that were both surfaced
by the code-quality review under #108.

Issue #114: three sites in joining.rs and listeners.rs acquired the
join_links Mutex via .lock().unwrap(). If any prior caller had panicked
while holding the guard, the lock would be poisoned and every future
caller — including the gossip listener that processes inbound
JoinRequest messages — would panic itself, permanently disabling that
path. Switch the Arc<Mutex<Vec<JoinLink>>> from std::sync::Mutex to
parking_lot::Mutex (added at workspace level and pulled into
crates/client only) and drop the .unwrap() calls. parking_lot does not
poison on holder panic, so subsequent acquisitions succeed. The new
test join_links_lock_survives_panic_in_holder demonstrates this.

Issue #115: accept_invite previously used unwrap_or_else(|_|
Uuid::new_v4()) and unwrap_or_else(|_| ChannelId::new()) to coerce
invalid invite fields into freshly minted IDs. The result was a
split-brain where the joining peer thought it had joined a server
that did not exist for any other peer. Add a
ClientError::MalformedInvite(String) variant (the first typed error in
the crate, derived via thiserror), validate the server id up front,
and propagate create_channel errors via ? from inside the registry
mutation closure. The new test malformed_invite_server_id_is_rejected
builds a real invite encrypted for the test client, tampers the
embedded server_id to "not-a-uuid", re-encodes it, and asserts that
accept_invite returns ClientError::MalformedInvite mentioning
server_id.

Validation: cargo fmt --check, cargo clippy --workspace -- -D warnings,
cargo test -p willow-client -p willow-state -p willow-channel
-p willow-network, and the full just check-wasm cohort (-p
willow-identity -p willow-state -p willow-channel -p willow-messaging
-p willow-crypto -p willow-transport -p willow-common -p willow-network
-p willow-client -p willow-web) all pass with zero warnings.

Closes #114
Closes #115
Progresses #108

https://claude.ai/code/session_018TTGhL645aTR4RZWrRBLPS
@intendednull intendednull force-pushed the claude/issue-114-115-client-panics branch from 57bdb86 to 9518c59 Compare April 11, 2026 16:36
@intendednull intendednull merged commit ef88993 into main Apr 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants