{"record":{"id":"9e4c6f28f581d4fe","repo":"srbhr/Resume-Matcher","slug":"resume-content-is-empty-after-text-extraction","errorCode":null,"errorMessage":"Resume content is empty after text extraction.","messagePattern":"Resume content is empty after text extraction\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"apps/backend/app/services/parser.py","lineNumber":158,"sourceCode":"    finally:\n        tmp_path.unlink(missing_ok=True)\n\n\nasync def parse_resume_to_json(markdown_text: str) -> dict[str, Any]:\n    \"\"\"Parse resume markdown to structured JSON using LLM.\n\n    After LLM parsing, patches any year-only dates with month-inclusive\n    dates extracted from the raw markdown. This ensures months are never\n    lost regardless of LLM behavior.\n\n    Args:\n        markdown_text: Resume content in markdown format\n\n    Returns:\n        Structured resume data matching ResumeData schema\n    \"\"\"\n    if not markdown_text or not markdown_text.strip():\n        raise ValueError(\"Resume content is empty after text extraction.\")\n\n    prompt = PARSE_RESUME_PROMPT.format(\n        schema=RESUME_SCHEMA_EXAMPLE,\n        resume_text=markdown_text,\n    )\n\n    config = get_llm_config()\n    model_name = get_model_name(config)\n    result = await complete_json(\n        prompt=prompt,\n        system_prompt=\"You are a JSON extraction engine. Output only valid JSON, no explanations.\",\n        max_tokens=get_safe_max_tokens(model_name),\n        retries=3,\n    )\n\n    # Patch dates: restore months the LLM may have dropped\n    result = restore_dates_from_markdown(result, markdown_text)\n","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/services/parser.py#L140-L176","documentation":"parse_resume_to_json requires non-empty markdown resume text because it will be sent to the LLM for structuring. If markdown_text is None, empty, or whitespace-only after upstream text extraction, it raises this ValueError instead of making a doomed LLM call.","triggerScenarios":"Uploading a resume file whose extraction produced no text (empty file, scanned/image-only PDF with no OCR text, corrupted docx), or calling parse_resume_to_json directly with \"\"/None; raised in paths reached via upload_resume and retry_processing.","commonSituations":"User uploads a scanned PDF (image-only, no text layer), a zero-byte file, or an unsupported format that the extractor silently returns empty for; OCR not configured.","solutions":["Verify the uploaded file has extractable text (e.g. run pdftotext / inspect extraction output) before parsing","Reject image-only/scanned PDFs at upload and ask the user for a text-based file, or enable OCR preprocessing","Check the file isn't empty/corrupt at upload time and return a 400/422 with a clear message","Log extraction output size to distinguish extractor failures from genuinely empty documents"],"exampleFix":"// before\ntext = extract_text(upload.file)\nawait parse_resume_to_json(text)  # ValueError if blank\n// after\ntext = extract_text(upload.file)\nif not text or not text.strip():\n    raise HTTPException(422, \"Could not extract text from the uploaded resume (scanned PDF?)\")\nawait parse_resume_to_json(text)","handlingStrategy":"try-catch","validationCode":"def extraction_ok(text: str | None) -> bool:\n    return bool(text and text.strip())","typeGuard":"def has_extractable_text(v: object) -> TypeGuard[str]:\n    return isinstance(v, str) and v.strip() != \"\"","tryCatchPattern":"try:\n    data = await parse_resume_to_json(markdown_text)\nexcept ValueError as e:\n    if \"empty after text extraction\" in str(e):\n        raise HTTPException(422, \"No readable text in the uploaded resume; try a text-based PDF/DOCX.\") from e\n    raise","preventionTips":["Detect scanned/image-only PDFs at upload (no text layer) and require OCR or a different file","Check file size > 0 and extractor output length before parsing","Show a clear upload error instead of proceeding to LLM parsing","Log extraction char counts to spot failing extractors"],"tags":["parsing","file-upload","empty-input"],"backgroundTag":"empty-document-extraction","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}