odysseus-dev/odysseus · error · Error

HTTP ${res.status}

Error message

HTTP ${res.status}

What it means

Thrown in _saveBuiltinEdit (static/js/skills.js:590) when PUT /api/skills/builtin/{name} with {text: ta.value} returns non-2xx; surfaced via showError('Save failed: HTTP N'). The route persists an override of a built-in capability's instructions, so failures typically mean the server rejected the write or the builtin no longer exists.

Source

Thrown at static/js/skills.js:590

  ta.value = (card._text != null ? card._text : (pre ? pre.textContent : '')) || '';
  ta.addEventListener('click', (e) => e.stopPropagation());
  if (pre) pre.style.display = 'none';
  preview.insertBefore(ta, preview.querySelector('.doclib-card-expanded-actions'));
  ta.focus();
  const editBtn = [...preview.querySelectorAll('.doclib-card-action-btn')].find(b => /Edit|Save/.test(b.textContent));
  if (editBtn) editBtn.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px;margin-right:3px;"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>Save';
}

async function _saveBuiltinEdit(card, name) {
  const ta = card.querySelector('.skill-md-editor');
  if (!ta) return;
  try {
    const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text: ta.value }),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    uiModule.showToast('Built-in capability updated');
    builtinSkills = [];  // force reload of built-in list (refreshes "edited" badge)
    await loadSkills();
  } catch (e) { uiModule.showError('Save failed: ' + e.message); }
}

async function _revertBuiltin(name) {
  if (!(await uiModule.styledConfirm(`Revert "${name}" to its original built-in instructions?`, { confirmText: 'Revert', danger: true }))) return;
  try {
    const res = await fetch(`${API}/api/skills/builtin/${encodeURIComponent(name)}`, { method: 'DELETE' });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    uiModule.showToast('Reverted to default');
    builtinSkills = [];
    await loadSkills();
  } catch (e) { uiModule.showError('Revert failed: ' + e.message); }
}

function _getFilteredSkills() {

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Match the HTTP code from the toast: 404 → reload skills (builtin renamed upstream); 422 → check the body shape is exactly {text: string}; 5xx → check write permissions on the overrides directory.
  2. Verify the server process can write to its builtin-overrides storage path.
  3. Re-login on 401 and retry — the textarea content is preserved in the DOM.
  4. After a server upgrade, hard-refresh the app so the builtin list matches the server.

Example fix

// before
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// after
const d = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(d.detail || `HTTP ${res.status}`);
Defensive patterns

Strategy: try-catch

Validate before calling

if (!ta.value.trim()) { uiModule.showError('Text required'); return; }

Try / catch

try { const res = await fetch(url, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ text: ta.value }) }); const d = await res.json().catch(() => ({})); if (!res.ok) throw new Error(d.detail || `HTTP ${res.status}`); } catch (e) { uiModule.showError('Save failed: ' + e.message); }

Prevention

When it happens

Trigger: Clicking Save in the built-in skill editor: PUT /api/skills/builtin/{name} with JSON body {text}. 404 when the builtin name is unknown (version skew), 401/403 without rights, 422 when `text` is missing or the wrong type, 500/507 when the override file cannot be written (read-only filesystem, disk full).

Common situations: Server container with a read-only volume for skill overrides; frontend bundle from a different release than the server; session expiry during a long editing session; empty textarea still sent as text:'' if the route requires non-empty.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/3cf42dbd3f5b54c0. Report an issue: GitHub.