odysseus-dev/odysseus · info · HTTPException
No documents specified
Error message
No documents specified
What it means
400 from POST /api/documents/export-zip: the JSON body must contain a non-empty ids array; the request either had no ids key, ids was null, or an empty list. Requests whose body fails to parse fall back to {} and also land here.
Source
Thrown at routes/document/document_routes.py:572
return {"ok": True, "id": doc_id, "extracted": True, "chars": len(body_text)}
finally:
db.close()
# ---- POST /api/documents/export-zip — bundle selected docs into a .zip ----
@router.post("/api/documents/export-zip")
async def documents_export_zip(request: Request):
"""Zip the selected documents (each as a text file with the right
extension) — mirrors the gallery's bulk download-zip so multi-export
is one file instead of a blocked flood of individual downloads."""
user = get_current_user(request)
try:
data = await request.json()
except Exception as e:
logger.warning("Failed to parse export request body, defaulting to empty", exc_info=e)
data = {}
ids = data.get("ids") or []
if not ids:
raise HTTPException(400, "No documents specified")
_ext = {
"javascript": ".js", "python": ".py", "html": ".html", "css": ".css",
"markdown": ".md", "json": ".json", "yaml": ".yml", "bash": ".sh",
"sql": ".sql", "rust": ".rs", "go": ".go", "java": ".java", "c": ".c",
"cpp": ".cpp", "typescript": ".ts", "ruby": ".rb", "php": ".php",
"text": ".txt", "xml": ".xml", "toml": ".toml", "ini": ".ini",
}
db = SessionLocal()
try:
import io
import re
import zipfile
from fastapi import Response
docs = db.query(Document).filter(Document.id.in_(ids)).all()
buf = io.BytesIO()
used = set()
wrote = 0
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:View on GitHub (pinned to f9235ebbf1)
Solutions
- Send {"ids": ["<doc-id>", ...]} with at least one id and Content-Type: application/json.
- Disable the export action in the UI until at least one document is selected.
- If the body is intentionally dynamic, JSON.stringify an explicit ids array rather than relying on defaults.
Example fix
// before
await api.post('/api/documents/export-zip', { documentIds: selected }); // wrong key -> 400
// after
await api.post('/api/documents/export-zip', { ids: selected.map(d => d.id) }); Defensive patterns
Strategy: validation
Validate before calling
const ids = selected.map(d => d.id).filter(Boolean);
if (!ids.length) { notify('Select at least one document'); return; }
await api.post('/api/documents/export-zip', { ids }); Type guard
function isValidExportBody(body: unknown): body is { ids: string[] } {
const b = body as { ids?: unknown };
return Array.isArray(b.ids) && b.ids.length > 0 && b.ids.every(i => typeof i === 'string' && i.length > 0);
} Try / catch
try { await api.post('/api/documents/export-zip', { ids }); }
catch (e) { if (e.status === 400 && /No documents specified/.test(e.message)) { notify('Nothing selected'); return; } throw e; } Prevention
- Disable export buttons until a selection exists
- Always send Content-Type: application/json with the ids array
- Name the key exactly 'ids'
When it happens
Trigger: Sending {} or {"ids": []}; malformed JSON body (wrong content-type, trailing commas) that silently degrades to the empty default; frontend sending ids under a different key (e.g. document_ids).
Common situations: Export button enabled with nothing selected; refactoring the payload schema without updating the export call; content-type text/plain causing request.json() to fail.
Related errors
- Provide at least one point, box, or object text
- Box must be [x1, y1, x2, y2]
- content is required
- markdown is required
- query is required
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/afa2aad81e20e713.
Report an issue: GitHub.