{"record":{"id":"d9ddd0de53382607","repo":"odysseus-dev/odysseus","slug":"no-documents-found","errorCode":null,"errorMessage":"No documents found","messagePattern":"No documents found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"routes/document/document_routes.py","lineNumber":608,"sourceCode":"            with zipfile.ZipFile(buf, \"w\", zipfile.ZIP_DEFLATED) as zf:\n                for doc in docs:\n                    try:\n                        _verify_doc_owner(db, doc, user)\n                    except HTTPException:\n                        continue   # skip docs the user doesn't own\n                    ext = _ext.get(doc.language or \"text\", \".txt\")\n                    base = (doc.title or \"document\").strip() or \"document\"\n                    base = re.sub(r\"[^\\w\\-. ]+\", \"\", base)[:60].strip() or doc.id\n                    name = base if \".\" in base else base + ext\n                    i = 1\n                    while name in used:\n                        name = f\"{base}-{i}\" + (\"\" if \".\" in base else ext)\n                        i += 1\n                    used.add(name)\n                    zf.writestr(name, doc.current_content or \"\")\n                    wrote += 1\n            if not wrote:\n                raise HTTPException(404, \"No documents found\")\n            return Response(\n                content=buf.getvalue(),\n                media_type=\"application/zip\",\n                headers={\"Content-Disposition\": 'attachment; filename=\"documents.zip\"'},\n            )\n        finally:\n            db.close()\n\n    # ---- PUT /api/document/{doc_id} — user manual edit ----\n    # Coalesce window: if the last user version was saved within this many\n    # seconds, update it in-place (user is still actively editing).\n    # Once the gap exceeds this, the next save creates a new version.\n    VERSION_COALESCE_SECONDS = 60\n\n    @router.put(\"/api/document/{doc_id}\")\n    async def update_document(request: Request, doc_id: str, req: DocumentUpdate) -> Dict[str, Any]:\n        user = get_current_user(request)\n        db = SessionLocal()","sourceCodeStart":590,"sourceCodeEnd":626,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/document/document_routes.py#L590-L626","documentation":"404 from POST /api/documents/export-zip: ids were supplied, but after querying and filtering the documents the loop wrote zero entries into the zip (wrote == 0). Typically the ids do not match any Document rows (deleted, wrong user's docs) — a partial match writes some files and does not raise.","triggerScenarios":"All requested ids are stale/deleted; ids belong to another owner and are filtered out; ids with whitespace or case differences that fail the equality match; empty strings in the ids array.","commonSituations":"Exporting a selection made before another session deleted those docs; shared id lists between users; copy-paste artifacts in ids.","solutions":["Re-fetch GET /api/documents/library and export ids that currently exist for this user.","Trim/normalize ids before sending; drop falsy entries.","Treat 404 here as 'selection is stale' and prompt a refresh rather than retrying the same ids."],"exampleFix":"// before\nawait api.post('/api/documents/export-zip', { ids: selectionFromLastHour }); // 404\n// after\nconst live = (await api.get('/api/documents/library')).documents.map(d => d.id);\nconst ids = selection.filter(id => live.includes(id));\nif (ids.length) await api.post('/api/documents/export-zip', { ids });","handlingStrategy":"validation","validationCode":"const live = new Set((await api.get('/api/documents/library')).documents.map(d => d.id));\nconst ids = requestedIds.map(s => s.trim()).filter(id => live.has(id));\nif (!ids.length) { refreshLibrary(); return; }","typeGuard":null,"tryCatchPattern":"try { return await api.post('/api/documents/export-zip', { ids }); }\ncatch (e) { if (e.status === 404 && /No documents found/.test(e.message)) { await refreshLibrary(); return null; } throw e; }","preventionTips":["Export only ids confirmed present in a fresh library fetch","Trim and drop falsy ids before sending","Prompt a list refresh instead of retrying the same stale ids"],"tags":["http-404","export","stale-ids","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}