odysseus-dev/odysseus · error · Error
Failed
Error message
Failed
What it means
Fallback error for a failed password change. The handler POSTs {current_password, new_password} to /api/auth/change-password; on non-OK it tries d.detail and falls back to the bare string 'Failed' when the error body has no detail field. Client-side validation (min length, match) has already run before this point.
Source
Thrown at static/js/settings.js:2098
const saveBtn = el('settings-pw-save');
const msgEl = el('settings-pw-msg');
if (saveBtn) {
saveBtn.addEventListener('click', async () => {
const cur = el('settings-pw-current').value;
const nw = el('settings-pw-new').value;
const conf = el('settings-pw-confirm').value;
msgEl.style.color = '';
if (!cur || !nw) { msgEl.textContent = 'Fill in all fields'; msgEl.style.color = 'var(--red)'; return; }
if (nw.length < _authPolicy.password_min_length) { msgEl.textContent = `Min ${_authPolicy.password_min_length} characters`; msgEl.style.color = 'var(--red)'; return; }
if (nw !== conf) { msgEl.textContent = 'Passwords don\'t match'; msgEl.style.color = 'var(--red)'; return; }
saveBtn.disabled = true;
try {
const res = await fetch('/api/auth/change-password', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ current_password: cur, new_password: nw })
});
if (!res.ok) { const d = await res.json(); throw new Error(d.detail || 'Failed'); }
msgEl.style.color = 'var(--green)';
msgEl.textContent = 'Password updated';
el('settings-pw-current').value = '';
el('settings-pw-new').value = '';
el('settings-pw-confirm').value = '';
} catch (e) {
msgEl.style.color = 'var(--red)';
msgEl.textContent = e.message;
} finally {
saveBtn.disabled = false;
}
});
}
// ── Two-Factor Authentication ──
const tfaContent = el('settings-2fa-content');
if (tfaContent) {
async function render2FA() {View on GitHub (pinned to f9235ebbf1)
Solutions
- Verify current password is correct and the new one meets server policy
- Check the response body in devtools — the real reason is usually there but discarded
- Include res.status in the thrown error so 'Failed' becomes actionable
- Catch fetch/network rejections separately from HTTP errors
Example fix
// before
if (!res.ok) { const d = await res.json(); throw new Error(d.detail || 'Failed'); }
// after
if (!res.ok) {
const d = await res.json().catch(() => ({}));
throw new Error(d.detail || `Failed (HTTP ${res.status})`);
} Defensive patterns
Strategy: try-catch
Validate before calling
if (!cur || !nw || nw !== conf) return;
if (nw.length < _authPolicy.password_min_length) return;
if (!navigator.onLine) { msgEl.textContent = 'Offline'; return; } Try / catch
try { const res = await fetch(...); if (!res.ok) { const d = await res.json().catch(() => ({})); throw new Error(d.detail || `Failed (HTTP ${res.status})`); } ... } catch (e) { msgEl.textContent = e.message; } Prevention
- Guard res.json() — unparseable bodies currently throw SyntaxError
- Mirror the server's full password policy client-side
- Include res.status in fallback errors
- Keep the saveBtn disable/enable in finally (already correct)
When it happens
Trigger: POST /api/auth/change-password returns non-2xx: wrong current password, new password violating server-side policy (complexity, reuse), 401 session expired — with a body lacking a usable detail field; or the fetch itself rejects (offline).
Common situations: User mistypes current password; server password policy stricter than the client checks; expired login; proxy strips the JSON error body.
Related errors
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/f50caa9624035c28.
Report an issue: GitHub.