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.

Solutions

  1. Verify current password is correct and the new one meets server policy
  2. Check the response body in devtools — the real reason is usually there but discarded
  3. Include res.status in the thrown error so 'Failed' becomes actionable
  4. 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

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.

Appendix: 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)