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

  1. Send {"ids": ["<doc-id>", ...]} with at least one id and Content-Type: application/json.
  2. Disable the export action in the UI until at least one document is selected.
  3. 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

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


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