fix(matrix): transient /sync errors misclassified as fatal auth — substring match hits the since= token (killed 5 bridges, 3 dead for 3 days) #130
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
dark/nether-voicebridge#130
Loading…
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?
Summary
A transient network error on
/syncis classified as a fatal auth error, because the classifier substring-matches the stringified error — and the string contains the sync URL, whose?since=<token>can itself contain401or403. The supervisor treats a fatal auth verdict as unrecoverable and kills the bridge with no retry.On 2026-08-11 01:17–01:26 UTC this killed five bridges on the public instance. Three stayed dead for three days until a manual
systemctl restart.Evidence
The bug,
crates/matrix-rtc/src/matrix.rs:1150-1153:What the bridges actually logged:
error sending request for urlis a reqwest transport error — there is no HTTP response, so there is no 401. The match fired on the sync token. Every bridge that died had a token in the5401xxxxrange:since=401Proof by contrast — the same underlying transport error two days later, with tokens that do not contain
401/403, correctly retried and logged only a WARN:So the failure is not "a bad night for the network" — it is a bad range of sync tokens. The landmine re-arms every time the homeserver's sync stream crosses a position containing
401or403.Second defect: a fatal verdict is permanent
crates/bridge/src/supervisor.rs:262logsbridge failedand the bridge is gone for the process lifetime. There is no restart path.The three static
config.tomlbridges (Light Voice, Light Gaming, Dark Voice) therefore stayed dead for three days. The three tenant bridges (Voice, fbi, bridge) came back only by accident: a self-serve link approval on 08-12 triggeredsupervisor: reconciling tenants against tenants.toml→tenant added — starting bridge. Static bridges have no equivalent path.This subsumes a previously-unfiled recurring gotcha: after a host reboot the bridge starts before Tuwunel is serving, every bridge's
m.login.application_servicegets502, and all bridges die permanently the same way. Both need the same defence.Fix
401, or errcodeM_UNKNOWN_TOKEN/M_MISSING_TOKEN;403only with errcodeM_FORBIDDEN. Never fatal: no HTTP response at all (transport error), 5xx, 429, timeouts. Keep the decision a pure function over(Option<u16>, Option<&str>)so it is unit-testable without a live SDK, with a thin adapter frommatrix_sdk::Error.crates/matrix-rtc/src/mgmt.rs:1242(management bot sync loop) — identical code, identical bug.base * 2^ncapped, mirroring the puppet join-timeout quarantine incrates/discord/src/puppet.rs:351), so that even a genuinely fatal error, a panic, or a startup 502 cannot leave a bridge permanently dead. Operator-initiated stops and shutdown must not auto-restart.crates/matrix-rtc/src/matrix.rs:583chooses a log message viamsg.contains("403"). Cosmetic today (both branches returnErr), but it will mislead on any room_id or URL containing403.Regression test
The incident itself: an error with no HTTP status whose message contains
since=54015213must classify as transient. Plus401/M_UNKNOWN_TOKEN→ fatal, and 502/timeout → transient.Related
Shipped in v0.3.2 (https://nether.codes/dark/nether-voicebridge/releases/tag/v0.3.2), deployed to prod 2026-08-14 18:16 UTC.
Prod verification: all 9 bridges report
joined Matrix roomafter the restart,/var/lib/nether-voicebridge/bridges.jsonlists 9/9 alive with itsupdatedstamp advancing every 60 s, and the ops digest now carriesbridges=9/9. The only journal ERROR since the restart is a pre-existing stale federated invite fromqwertyad.one, unrelated to this work.Live fault-injection results (run on the build host against the test fixture, prod untouched):
Matrix sync error — retrying in 5 s, zerobridge failed, bridge recovered on its own;401 Unauthorizedstill classified fatal → automatic restarts at 5 / 10 / 20 / 40 / 80 s, roster showingrestarting,consecutive_failures=5.