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
- 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.
- Verify the server process can write to its builtin-overrides storage path.
- Re-login on 401 and retry — the textarea content is preserved in the DOM.
- 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
- Send {text} exactly; do not invent extra fields the route 422s on.
- Check server write permissions on the overrides directory.
- Preserve textarea content on failure for retry.
- Re-fetch the builtin list after server upgrades before editing.
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.