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

  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.