health: probe /api/config, and report when the target is not a Vaultwarden

Was /alive, which answers on a Vaultwarden and proves only that SOMETHING is
listening on that address. /api/config self-identifies:

  {"version":"2026.6.0","server":{"name":"Vaultwarden",...}}

That distinction is the point of the check. The failure worth catching is not
"the box is down" — that surfaces on its own the moment anything else fails —
it is pointing the vault at the wrong port on a host running several things,
where a 200 from an unrelated service reads as success and the real problem
appears later as a login that cannot work.

So the panel now has three states rather than two: reachable and identified,
reachable but NOT a Vaultwarden (amber, with what to check), or no answer. The
server version is shown when it identifies itself.

Only /api/config is parsed, and only for `server.name` and `version`. It is
unauthenticated and carries no vault data — the hard rule about never reading
bodies governs the proxy path, and this is not on it.

Also recorded in-code: Vaultwarden does not implement /api/status, which does
exist in Bitwarden's official cloud API, so a probe aimed there 404s against a
perfectly healthy server. That near-miss looks exactly like a wrong address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 13:56:35 +00:00
co-authored by Claude Opus 5
parent 2f2529d8d5
commit 680b8fb2ba
2 changed files with 58 additions and 4 deletions
+18 -1
View File
@@ -25,6 +25,11 @@ type HealthResult = {
upstreamStatus?: number;
ms?: number;
url?: string;
/** `server.name` from Vaultwarden's own `/api/config`, or null if it did not identify itself. */
server?: string | null;
version?: string | null;
/** Answered 200 AND said it is a Vaultwarden. False means something else is on that address. */
identified?: boolean;
};
const Field = ({ label, children }: { label: string; children: React.ReactNode }) => (
@@ -230,10 +235,22 @@ export const VaultPanel = () => {
{health ? (
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
<div className="flex items-center gap-2">
<Pill tone={health.ok ? 'success' : 'danger'}>{health.ok ? 'Answering' : 'No answer'}</Pill>
{/* Three outcomes, not two. "Answered but is not a Vaultwarden" is its own state and the
main reason this check exists — pointing at the wrong port on a busy host gives a
perfectly good 200 from something that can never serve a vault. */}
<Pill tone={health.ok ? (health.identified ? 'success' : 'warning') : 'danger'}>
{health.ok ? (health.identified ? 'Vaultwarden' : 'Answered, but not a Vaultwarden') : 'No answer'}
</Pill>
{health.version && <span>v{health.version}</span>}
{health.ms !== undefined && <span>{health.ms}ms</span>}
{health.upstreamStatus !== undefined && <span>HTTP {health.upstreamStatus}</span>}
</div>
{health.ok && !health.identified && (
<span className={toneText.warning}>
Something is listening there, but it did not identify itself as a Vaultwarden. Check the
port.
</span>
)}
{health.error && <span className={toneText.danger}>{health.error}</span>}
</div>
) : (