{"record":{"id":"2f17415baf340cad","repo":"odysseus-dev/odysseus","slug":"import-failed-server-returned-res-status","errorCode":null,"errorMessage":"Import failed: server returned ${res.status}","messagePattern":"Import failed: server returned (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/admin.js","lineNumber":2831,"sourceCode":"    const msg = el('adm-backupMsg');\n    const btn = el('adm-importDataBtn');\n    btn.disabled = true; btn.textContent = 'Importing...'; msg.textContent = '';\n    try {\n      const text = (await file.text()).replace(/^\\uFEFF/, '').trim();\n      let data;\n      try {\n        data = JSON.parse(text);\n      } catch (e) {\n        throw new Error('Invalid backup file: ' + e.message);\n      }\n      const res = await fetch('/api/import', {\n        method: 'POST', credentials: 'same-origin',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify(data),\n      });\n      const result = await res.json().catch(() => null);\n      if (!result) {\n        throw new Error(`Import failed: server returned ${res.status}`);\n      }\n      if (res.ok && result.ok) {\n        msg.textContent = result.message || 'Import successful.'; msg.className = 'admin-success';\n      } else {\n        msg.textContent = result.message || result.detail || 'Import failed'; msg.className = 'admin-error';\n      }\n    } catch (e) { msg.textContent = 'Import failed: ' + e.message; msg.className = 'admin-error'; }\n    btn.disabled = false; btn.textContent = 'Import Data';\n  });\n}\n\n/* ── Danger Zone ── */\nfunction initDangerZone() {\n  // Per-category Danger Zone wipes. Each button declares its target\n  // via data-wipe-kind; one delegated handler handles double-confirm,\n  // POSTs to /api/admin/wipe/{kind}, and writes the result.\n  const _LABELS = {\n    chats: 'chats', memory: 'memory entries', skills: 'skills',","sourceCodeStart":2813,"sourceCodeEnd":2849,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/admin.js#L2813-L2849","documentation":"Raised by Skills.install_bundle when the files dict passed in is empty (falsy). This is the earliest guard before pick_skill_md/from_markdown run — an empty bundle means nothing to install. Like error 986 it only fires on direct calls: fetch_skill_bundle either returns non-empty files or raises error 985 first.","triggerScenarios":"Calling install_bundle({}) or install_bundle(files) where files was populated by a loop/traversal that matched nothing (empty directory walk, filtered-out extensions, failed downloads swallowed into an empty map).","commonSituations":"Custom import pipelines that glob a local directory which is empty or whose files were all filtered out; race where the source directory was deleted between listing and install; passing fetch output after a failed fetch that returned {} instead of raising.","solutions":["Populate the bundle before installing: fetch via fetch_skill_bundle, or verify your local traversal actually collected files","Check intermediate filters (extension allowlists, size checks) — they may be excluding everything","Fail earlier with a clearer message at the collection site rather than passing an empty dict into install_bundle","Log the dict size/keys just before calling install_bundle to confirm what the traversal produced"],"exampleFix":"# before\nfiles = {p.read_text('utf-8') for p in dir.glob('*.x') if keep(p)}  # keep() rejects all\nskills.install_bundle(files)  # 'empty bundle'\n\n# after\nfiles = {p.name: p.read_text('utf-8') for p in dir.glob('SKILL.md')}\nif not files:\n    raise ValueError(f\"no SKILL.md collected from {dir}\")\nskills.install_bundle(files)","handlingStrategy":"validation","validationCode":"def is_installable_bundle(files: dict[str, str]) -> bool:\n    return bool(files) and any(p.lower().endswith(\"skill.md\") for p in files)","typeGuard":"from typing import TypeGuard\n\ndef is_non_empty_bundle(files: dict[str, str]) -> TypeGuard[dict[str, str]]:\n    return len(files) > 0","tryCatchPattern":"try:\n    skills.install_bundle(files)\nexcept SkillImportError as e:\n    if str(e) == \"empty bundle\":\n        raise ValueError(\"collected nothing to install — check source path and filters\") from e\n    raise","preventionTips":["Validate the collection step produced files before calling install_bundle","Warn loudly when traversal filters exclude everything instead of silently returning {}","Log len(files) and its keys right before install in custom pipelines"],"tags":["validation","skill-import","empty-input"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}