{"record":{"id":"afa2aad81e20e713","repo":"odysseus-dev/odysseus","slug":"no-documents-specified","errorCode":null,"errorMessage":"No documents specified","messagePattern":"No documents specified","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"info","filePath":"routes/document/document_routes.py","lineNumber":572,"sourceCode":"            return {\"ok\": True, \"id\": doc_id, \"extracted\": True, \"chars\": len(body_text)}\n        finally:\n            db.close()\n\n    # ---- POST /api/documents/export-zip — bundle selected docs into a .zip ----\n    @router.post(\"/api/documents/export-zip\")\n    async def documents_export_zip(request: Request):\n        \"\"\"Zip the selected documents (each as a text file with the right\n        extension) — mirrors the gallery's bulk download-zip so multi-export\n        is one file instead of a blocked flood of individual downloads.\"\"\"\n        user = get_current_user(request)\n        try:\n            data = await request.json()\n        except Exception as e:\n            logger.warning(\"Failed to parse export request body, defaulting to empty\", exc_info=e)\n            data = {}\n        ids = data.get(\"ids\") or []\n        if not ids:\n            raise HTTPException(400, \"No documents specified\")\n        _ext = {\n            \"javascript\": \".js\", \"python\": \".py\", \"html\": \".html\", \"css\": \".css\",\n            \"markdown\": \".md\", \"json\": \".json\", \"yaml\": \".yml\", \"bash\": \".sh\",\n            \"sql\": \".sql\", \"rust\": \".rs\", \"go\": \".go\", \"java\": \".java\", \"c\": \".c\",\n            \"cpp\": \".cpp\", \"typescript\": \".ts\", \"ruby\": \".rb\", \"php\": \".php\",\n            \"text\": \".txt\", \"xml\": \".xml\", \"toml\": \".toml\", \"ini\": \".ini\",\n        }\n        db = SessionLocal()\n        try:\n            import io\n            import re\n            import zipfile\n            from fastapi import Response\n            docs = db.query(Document).filter(Document.id.in_(ids)).all()\n            buf = io.BytesIO()\n            used = set()\n            wrote = 0\n            with zipfile.ZipFile(buf, \"w\", zipfile.ZIP_DEFLATED) as zf:","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/document/document_routes.py#L554-L590","documentation":"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.","triggerScenarios":"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).","commonSituations":"Export button enabled with nothing selected; refactoring the payload schema without updating the export call; content-type text/plain causing request.json() to fail.","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."],"exampleFix":"// before\nawait api.post('/api/documents/export-zip', { documentIds: selected }); // wrong key -> 400\n// after\nawait api.post('/api/documents/export-zip', { ids: selected.map(d => d.id) });","handlingStrategy":"validation","validationCode":"const ids = selected.map(d => d.id).filter(Boolean);\nif (!ids.length) { notify('Select at least one document'); return; }\nawait api.post('/api/documents/export-zip', { ids });","typeGuard":"function isValidExportBody(body: unknown): body is { ids: string[] } {\n  const b = body as { ids?: unknown };\n  return Array.isArray(b.ids) && b.ids.length > 0 && b.ids.every(i => typeof i === 'string' && i.length > 0);\n}","tryCatchPattern":"try { await api.post('/api/documents/export-zip', { ids }); }\ncatch (e) { if (e.status === 400 && /No documents specified/.test(e.message)) { notify('Nothing selected'); return; } throw e; }","preventionTips":["Disable export buttons until a selection exists","Always send Content-Type: application/json with the ids array","Name the key exactly 'ids'"],"tags":["http-400","validation","export","request-body"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}