{"record":{"id":"718e8949bd495209","repo":"odysseus-dev/odysseus","slug":"document-is-not-linked-to-a-source-pdf","errorCode":null,"errorMessage":"Document is not linked to a source PDF","messagePattern":"Document is not linked to a source PDF","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/document/document_routes.py","lineNumber":1083,"sourceCode":"    async def export_pdf_preview(doc_id: str, request: Request) -> Dict[str, Any]:\n        \"\"\"Return the field-value mapping that would be written to the PDF.\n\n        Frontend shows this in a confirmation modal so the user can spot/fix\n        any wrong values before triggering the actual download.\n        \"\"\"\n        from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar\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            upload_id = find_source_upload_id(doc.current_content or \"\")\n            if not upload_id:\n                raise HTTPException(400, \"Document is not linked to a source PDF\")\n\n            pdf_path = _locate_current_user_upload(request, upload_id, user)\n            if not pdf_path:\n                raise HTTPException(404, f\"Source PDF {upload_id} not found in uploads\")\n\n            fields = load_field_sidecar(pdf_path)\n            if not fields:\n                raise HTTPException(404, \"Field schema sidecar missing for source PDF\")\n\n            values = parse_markdown_to_values(doc.current_content or \"\")\n            field_meta = {f[\"name\"]: f for f in fields}\n\n            preview = []\n            for name, current in values.items():\n                meta = field_meta.get(name)\n                if not meta:\n                    continue\n                preview.append({","sourceCodeStart":1065,"sourceCodeEnd":1101,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/document/document_routes.py#L1065-L1101","documentation":"Raised by POST /api/document/{doc_id}/export-pdf/preview with status 400 when find_source_upload_id(doc.current_content) returns None — the document's markdown content contains no marker linking it to an uploaded source PDF. Only documents created from a PDF form upload (whose content embeds the upload reference) support the export-preview flow.","triggerScenarios":"Running export preview on a free-typed or AI-generated document that never came from a PDF upload; the linking marker in current_content was edited away or reformatted by a save; the document was restored from an old version that predates the PDF link.","commonSituations":"UI offers the export-PDF action for all documents instead of only PDF-derived ones; heavy content edits strip the source marker.","solutions":["Only expose the export-PDF action when the document is PDF-derived (the marker is present).","If the marker was lost to edits, restore the version that still contains the source-upload reference.","Recreate the document via the original PDF upload flow to re-establish the link.","Return the user to a plain markdown/file export for non-PDF documents."],"exampleFix":"// before\n<button onClick={() => exportPdf(doc)}>Export PDF</button>\n\n// after\nconst isPdfDoc = /\\[\\[?source[_ -]?upload:([\\w-]+)\\]?\\]/i.test(doc.current_content || '');\n{isPdfDoc && <button onClick={() => exportPdf(doc)}>Export PDF</button>}","handlingStrategy":"type-guard","validationCode":"import re\n\nSOURCE_MARKER = re.compile(r\"source[_ -]?upload[:\\s]*([\\\\w-]+)\", re.IGNORECASE)\n\ndef is_pdf_derived(content: str | None) -> bool:\n    return bool(content and SOURCE_MARKER.search(content))","typeGuard":"def is_pdf_derived(doc: Document | None) -> bool:\n    \"\"\"Type/narrowing guard: only PDF-derived docs support export-preview.\"\"\"\n    return bool(doc is not None and doc.current_content and SOURCE_MARKER.search(doc.current_content))","tryCatchPattern":"try:\n    p = requests.post(f\"{base}/api/document/{doc}/export-pdf/preview\")\nexcept requests.HTTPError as e:\n    if e.response.status_code == 400:  # not PDF-derived\n        offer_markdown_export(doc)\n    else:\n        raise","preventionTips":["Show the Export-PDF action only for PDF-derived documents.","Ensure edits/AI rewrites preserve the source-upload marker in content.","If a restore drops the marker, restore a newer version that has it."],"tags":["fastapi","http-400","pdf-export","validation"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}