{"record":{"id":"55d5d8e48c4e4415","repo":"srbhr/Resume-Matcher","slug":"could-not-extract-text-from-the-uploaded-file-the","errorCode":null,"errorMessage":"Could not extract text from the uploaded file. The document may be image-based or scanned. Please upload a text-based PDF/DOCX with selectable text, or run OCR first.","messagePattern":"Could not extract text from the uploaded file\\. The document may be image-based or scanned\\. Please upload a text-based PDF/DOCX with selectable text, or run OCR first\\.","errorType":"http","errorClass":"HTTPException","httpStatus":422,"severity":"error","filePath":"apps/backend/app/routers/resumes.py","lineNumber":667,"sourceCode":"            detail=f\"File too large. Maximum size: {MAX_FILE_SIZE // (1024 * 1024)}MB\",\n        )\n\n    if len(content) == 0:\n        raise HTTPException(status_code=400, detail=\"Empty file\")\n\n    # Convert to markdown\n    try:\n        markdown_content = await parse_document(content, file.filename or \"resume.pdf\")\n    except Exception as e:\n        logger.error(f\"Document parsing failed: {e}\")\n        raise HTTPException(\n            status_code=422,\n            detail=\"Failed to parse document. Please ensure it's a valid PDF or DOCX file.\",\n        )\n\n    # Validate extracted text is not empty (image-based PDFs / scanned documents)\n    if not markdown_content or not markdown_content.strip():\n        raise HTTPException(\n            status_code=422,\n            detail=(\n                \"Could not extract text from the uploaded file. The document may be \"\n                \"image-based or scanned. Please upload a text-based PDF/DOCX with \"\n                \"selectable text, or run OCR first.\"\n            ),\n        )\n\n    # Store in database first with \"processing\" status (atomic master assignment)\n    # original_markdown is preserved permanently for date reference even after\n    # builder saves overwrite `content` with JSON.\n    resume = await db.create_resume_atomic_master(\n        content=markdown_content,\n        content_type=\"md\",\n        filename=file.filename,\n        processed_data=None,\n        processing_status=\"processing\",\n        original_markdown=markdown_content,","sourceCodeStart":649,"sourceCodeEnd":685,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/routers/resumes.py#L649-L685","documentation":"upload_resume raises this HTTP 422 after document parsing succeeded but produced no extractable text. It specifically guards against image-based or scanned PDFs where markdown_content is empty or whitespace-only, since downstream analysis requires selectable text.","triggerScenarios":"POSTing a resume upload (POST to the resumes upload endpoint) with a PDF or DOCX whose pages are raster images (scanned document, photographed page, screenshot-exported PDF), so the parser extracts an empty/whitespace-only markdown_content.","commonSituations":"Users scan paper resumes into PDFs; resume exported from design tools as flattened images; faxed or photographed documents; PDF with embedded fonts the extractor cannot decode; OCR preprocessing was skipped.","solutions":["Run OCR on the document (e.g. ocrmypdf input.pdf output.pdf) and re-upload the OCR'd file","Re-export the resume from its source application as a text-based PDF (Save As PDF, not print-to-image)","Upload a DOCX version instead, which almost always contains selectable text","Verify locally that text extraction works before uploading (pdftotext file.pdf returns non-empty output)"],"exampleFix":"// before: raw scanned PDF from flatbed scanner\nupload_resume(file=open('scan.pdf','rb'))  # 422 image-based\n// after\nsubprocess.run(['ocrmypdf','scan.pdf','scan-ocr.pdf'])\nupload_resume(file=open('scan-ocr.pdf','rb'))","handlingStrategy":"validation","validationCode":"async function hasSelectableText(file: File): Promise<boolean> {\n  if (file.type === 'application/pdf') {\n    const buf = await file.arrayBuffer();\n    const pdf = await pdfjsLib.getDocument({data: buf}).promise;\n    for (let i = 1; i <= Math.min(pdf.numPages, 3); i++) {\n      const tc = await (await pdf.getPage(i)).getTextContent();\n      if (tc.items.some(it => it.str.trim())) return true;\n    }\n    return false;\n  }\n  return true; // DOCX is text-based\n}\nif (!await hasSelectableText(file)) alert('Run OCR before uploading');","typeGuard":"function isTextBased(doc: {extractedText?: string | null}): doc is {extractedText: string} {\n  return typeof doc.extractedText === 'string' && doc.extractedText.trim().length > 0;\n}","tryCatchPattern":"try {\n  await api.uploadResume(file);\n} catch (e) {\n  if (e.response?.status === 422 && /image-based|OCR/.test(e.response.data?.detail ?? '')) {\n    showOcrInstructions(); // e.g. link to ocrmypdf / online OCR\n  } else throw e;\n}","preventionTips":["Only upload text-based PDFs (created via Save As PDF, not scans or print-to-image)","Pre-check selectable text locally (select text in the PDF viewer; pdftotext returns output)","Run ocrmypdf on any scanned document before upload","Prefer DOCX when the source document is editable"],"tags":["http-422","document-parsing","ocr","pdf","empty-text-extraction"],"backgroundTag":"image-based-pdf-no-extractable-text","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}