From 0ec9e8b8a04152f6ed266ca92458ed746d557d1a Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 22 Jun 2026 13:22:06 +0200 Subject: [PATCH] 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. --- .../packages/dashboard/src/matrix-accounts.js | 50 +++++++++++++++---- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/frontend/packages/dashboard/src/matrix-accounts.js b/frontend/packages/dashboard/src/matrix-accounts.js index 92fe2159..26db0a9a 100644 --- a/frontend/packages/dashboard/src/matrix-accounts.js +++ b/frontend/packages/dashboard/src/matrix-accounts.js @@ -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: "" } 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) {