{"record":{"id":"58a9561a21868185","repo":"odysseus-dev/odysseus","slug":"document-is-not-a-pdf-no-pdf-source-marker-found","errorCode":null,"errorMessage":"Document is not a PDF — no pdf_source marker found","messagePattern":"Document is not a PDF — no pdf_source marker found","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"info","filePath":"routes/document/document_routes.py","lineNumber":523,"sourceCode":"        Lets the AI see PDF contents for old docs that were imported before\n        text extraction was wired, plus for scanned/image-only PDFs where the\n        VL model picks up text the basic pypdf path missed.\"\"\"\n        import re\n        from src.document_processor import _process_pdf, strip_pdf_content_marker\n        from src.pdf_form_doc import find_source_upload_id\n\n        user = get_current_user(request)\n        db = SessionLocal()\n        try:\n            doc = db.query(Document).filter(Document.id == doc_id).first()\n            if not doc:\n                raise HTTPException(404, \"Document not found\")\n            _verify_doc_owner(db, doc, user)\n\n            content = doc.current_content or \"\"\n            upload_id = find_source_upload_id(content)\n            if not upload_id:\n                raise HTTPException(400, \"Document is not a PDF — no pdf_source marker found\")\n\n            pdf_path = _locate_current_user_upload(request, upload_id, user)\n            if not pdf_path:\n                raise HTTPException(404, \"Source PDF could not be located\")\n\n            try:\n                body_text = strip_pdf_content_marker(_process_pdf(pdf_path, owner=user))\n            except Exception as e:\n                logger.error(f\"extract_pdf_text failed for {pdf_path}: {e}\")\n                raise HTTPException(500, f\"Extraction failed: {e}\")\n\n            if not body_text:\n                return {\"ok\": True, \"id\": doc_id, \"extracted\": False, \"reason\": \"No readable content\"}\n\n            # Preserve everything up through the title (front-matter marker +\n            # first H1) and replace the rest with the freshly extracted text.\n            head_re = re.compile(r'^(<!--[^>]+-->\\s*\\n+#[^\\n]*\\n+)', re.MULTILINE)\n            head_match = head_re.match(content)","sourceCodeStart":505,"sourceCodeEnd":541,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/document/document_routes.py#L505-L541","documentation":"400 from POST /api/document/{doc_id}/extract-pdf-text: the document exists and is owned by the caller, but its current_content contains no pdf_source marker that find_source_upload_id can parse. The endpoint only works on documents originally imported from a PDF; text-native or otherwise-created docs have no linked upload.","triggerScenarios":"Running extraction on a markdown/text document created directly (POST /api/documents); a PDF-imported doc whose content was later fully overwritten by an edit that dropped the marker; very old docs imported before the pdf_source marker convention existed.","commonSituations":"Bulk 'extract all' scripts hitting mixed libraries; UI offering the extract action on every doc regardless of origin; content-rewrite tools stripping HTML comments (the marker is an HTML comment) during transforms.","solutions":["Only offer/attempt extraction for docs whose content contains the pdf_source marker (or that were created by import-pdf).","For legacy PDF docs without the marker, re-import the PDF to create a properly linked doc.","If content edits must preserve provenance, keep the marker intact when rewriting doc content.","Handle the 400 as 'not applicable' in automation rather than as a failure."],"exampleFix":"// before\nawait api.post(`/api/document/${doc.id}/extract-pdf-text`); // 400 on text docs\n// after\nconst isPdfDoc = /<!--\\s*pdf_source/.test(doc.current_content);\nif (isPdfDoc) await api.post(`/api/document/${doc.id}/extract-pdf-text`);","handlingStrategy":"type-guard","validationCode":"const isPdfDoc = /<!--\\s*pdf_source[^>]*-->/.test(doc.current_content ?? '');\nif (!isPdfDoc) return; // skip extraction for non-PDF docs","typeGuard":"function isPdfBackedDoc(doc: { current_content?: string | null }): boolean {\n  return /pdf_source/.test(doc.current_content ?? '');\n}","tryCatchPattern":"try { await api.post(`/api/document/${id}/extract-pdf-text`); }\ncatch (e) { if (e.status === 400 && /not a PDF/.test(e.message)) return; /* expected for text docs */ throw e; }","preventionTips":["Gate the extract action on the pdf_source marker in the UI","Preserve the marker when programmatically rewriting doc content","Treat this 400 as 'not applicable', never as a retryable failure"],"tags":["http-400","pdf","content-marker","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}