{"record":{"id":"770e8b3937d67dcd","repo":"srbhr/Resume-Matcher","slug":"only-resumes-with-failed-or-processing-status","errorCode":null,"errorMessage":"Only resumes with 'failed' or 'processing' status can be retried.","messagePattern":"Only resumes with 'failed' or 'processing' status can be retried\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"apps/backend/app/routers/resumes.py","lineNumber":1690,"sourceCode":"    if not await db.delete_resume(resume_id):\n        raise HTTPException(status_code=404, detail=\"Resume not found\")\n\n    return {\"message\": \"Resume deleted successfully\"}\n\n\n@router.post(\"/{resume_id}/retry-processing\", response_model=ResumeUploadResponse)\nasync def retry_processing(resume_id: str) -> ResumeUploadResponse:\n    \"\"\"Retry AI processing for a failed or stuck resume.\n\n    Re-runs parse_resume_to_json() on the stored markdown content.\n    Works for resumes with processing_status == \"failed\" or \"processing\".\n    \"\"\"\n    resume = await db.get_resume(resume_id)\n    if not resume:\n        raise HTTPException(status_code=404, detail=\"Resume not found\")\n\n    if resume.get(\"processing_status\") not in (\"failed\", \"processing\"):\n        raise HTTPException(\n            status_code=400,\n            detail=\"Only resumes with 'failed' or 'processing' status can be retried.\",\n        )\n\n    markdown_content = resume.get(\"content\", \"\")\n    if not markdown_content:\n        raise HTTPException(\n            status_code=400,\n            detail=\"Resume has no stored content to re-process.\",\n        )\n\n    try:\n        processed_data = await parse_resume_to_json(markdown_content)\n        await db.update_resume(\n            resume_id,\n            {\n                \"processed_data\": processed_data,\n                \"processing_status\": \"ready\",","sourceCodeStart":1672,"sourceCodeEnd":1708,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/routers/resumes.py#L1672-L1708","documentation":"HTTP 400 raised by the resume retry endpoint (retry_processing) when the target resume's processing_status is not 'failed' or 'processing'. Retry only makes sense for resumes whose processing actually failed or is stuck mid-flight; resuming a completed ('completed') or fresh resume would duplicate work or corrupt state. The library throws this as a guard before invoking the AI re-parse pipeline.","triggerScenarios":"PATCH/POST to the resume retry endpoint with a resume_id whose stored processing_status is 'completed', 'pending', or any value other than 'failed' or 'processing'.","commonSituations":"User clicks a 'Retry' button on a successfully processed resume; double-clicking retry after the first retry already finished (status became 'completed'); frontend state stale relative to the DB; status field names changed in a schema migration so the check falls through.","solutions":["Check the resume's processing_status (GET resume detail) before calling the retry endpoint and only offer retry when it is 'failed' or 'processing'.","If the resume is stuck in 'processing', wait for or investigate the background job; retry is only valid while it remains non-terminal.","If the resume already completed successfully, re-upload or use the reprocess endpoint instead of retry.","Refresh frontend status state (poll/websocket) so the retry button is disabled for ineligible resumes."],"exampleFix":"// before: blind retry\nawait api.post(`/resumes/${id}/retry`);\n\n// after: pre-check status\nconst r = await api.get(`/resumes/${id}`);\nif (['failed', 'processing'].includes(r.data.processing_status)) {\n  await api.post(`/resumes/${id}/retry`);\n}","handlingStrategy":"validation","validationCode":"const resume = await api.get(`/resumes/${id}`);\nif (!['failed', 'processing'].includes(resume.data.processing_status)) {\n  throw new Error(`Cannot retry: status is '${resume.data.processing_status}'`);\n}","typeGuard":"const canRetry = (r) => r != null && ['failed', 'processing'].includes(r.processing_status);","tryCatchPattern":"try {\n  await api.post(`/resumes/${id}/retry`);\n} catch (e) {\n  if (e.response?.status === 400) {\n    console.warn('Resume not in a retryable state; refresh status.');\n    await refreshResumeStatus(id);\n  } else throw e;\n}","preventionTips":["Poll or subscribe to processing_status and disable the retry button for non-retryable states.","Treat 'completed' as terminal — never offer retry.","Refresh cached resume state after any background-processing webhook/event."],"tags":["http-400","resume-processing","invalid-state"],"backgroundTag":"invalid-resume-status-for-retry","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}