dashboard(matrix-accounts): shape-agnostic login error bodies

The matrix-account-login endpoint returns failures as a bare plain-text
body today (hive-c0re error_response). The RFC 9457 rework moves it to
application/problem+json. The submit handler previously called
resp.json() on the error path, which threw on the plain-text body and
collapsed every real failure to a generic 'login failed (HTTP 500)',
hiding the actual reason.

Read the error body shape-agnostically: parse JSON only on 2xx for the
success envelope; on failure read the body once as text and, if it
parses as JSON, surface problem+json 'detail' (then 'error'/'title'
fallbacks), else use the raw text. This handles both the current
plain-text and the future problem+json shapes with no BE/FE merge-order
coupling. Header contract doc updated to match.
This commit is contained in:
iris 2026-06-22 13:22:06 +02:00 committed by mara
commit 0ec9e8b8a0

View file

@ -12,8 +12,12 @@
// POST /api/matrix-account-login (x-www-form-urlencoded, operator-auth)
// fields: agent, account, homeserver, mode=password|token,
// user_id?, password?, token?
// -> 2xx { ok: true, user_id } on success
// -> 4xx { error: "<msg>" } on failure
// -> 200 JSON { ok: true, user_id } on success
// -> error body in transition: today a bare plain-text body
// (hive-c0re's error_response), migrating to RFC 9457
// application/problem+json { type, title, detail, … }. The error
// path reads shape-agnostically (text first, then JSON `detail` if
// it parses) so both shapes work regardless of BE/FE merge order.
// The token is NEVER echoed back in any response, and this page never
// re-renders a submitted secret.
//
@ -198,17 +202,43 @@ async function submitLogin(e) {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(fd),
});
let body = {};
try { body = await resp.json(); } catch { /* tolerate non-JSON error pages */ }
if (resp.ok && body.ok) {
out.className = 'ma-result ok';
out.textContent = '✓ logged in as ' + (body.user_id || '(unknown)') + ' — token stored.';
clearSecrets(formEl);
loadAccounts(agent);
if (resp.ok) {
// Success is 200 + JSON { ok, user_id }.
let body = {};
try { body = await resp.json(); } catch { /* tolerate odd 2xx body */ }
if (body.ok) {
out.className = 'ma-result ok';
out.textContent = '✓ logged in as ' + (body.user_id || '(unknown)') + ' — token stored.';
clearSecrets(formEl);
loadAccounts(agent);
} else {
out.className = 'ma-result err';
out.textContent = '✗ login failed (unexpected response).';
clearSecrets(formEl);
}
} else {
// The BE error-body shape is in transition: today hive-c0re's
// error_response sends a bare plain-text body (e.g. "matrix-account-
// login: password mode needs user_id + password"); the RFC 9457 rework
// moves it to application/problem+json ({ type, title, detail, … }).
// Read shape-agnostically so the FE handles both with no merge-order
// coupling: pull the body once as text, and if it parses as JSON
// surface `detail` (problem+json) → `error`/`title` fallback, else use
// the raw text. A bare HTTP code is the last resort.
let msg = '';
try {
const raw = (await resp.text()).trim();
msg = raw;
if (raw && (raw[0] === '{' || raw[0] === '[')) {
try {
const body = JSON.parse(raw);
msg = body.detail || body.error || body.title || raw;
} catch { /* not JSON after all — keep the raw text */ }
}
} catch { /* fall back to the status code below */ }
out.className = 'ma-result err';
out.textContent = '✗ ' + (body.error || ('login failed (HTTP ' + resp.status + ')'));
out.textContent = '✗ ' + (msg || ('login failed (HTTP ' + resp.status + ')'));
clearSecrets(formEl);
}
} catch (err) {