{"record":{"id":"71bf15126a944bac","repo":"srbhr/Resume-Matcher","slug":"preview-required-before-confirmation-please-retry","errorCode":null,"errorMessage":"Preview required before confirmation. Please retry preview.","messagePattern":"Preview required before confirmation\\. Please retry preview\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"apps/backend/app/routers/resumes.py","lineNumber":1171,"sourceCode":"        preview_hashes = job.get(\"preview_hashes\")\n        allowed_hashes: set[str] = set()\n        if isinstance(preview_hashes, dict):\n            allowed_hashes.update(preview_hashes.values())\n        elif isinstance(preview_hashes, list):\n            allowed_hashes.update(\n                [value for value in preview_hashes if isinstance(value, str)]\n            )\n        else:\n            preview_hash = job.get(\"preview_hash\")\n            if isinstance(preview_hash, str):\n                allowed_hashes.add(preview_hash)\n\n        if not allowed_hashes:\n            logger.warning(\n                \"Rejecting confirm; preview hash missing for job %s.\",\n                request.job_id,\n            )\n            raise HTTPException(\n                status_code=400,\n                detail=\"Preview required before confirmation. Please retry preview.\",\n            )\n\n        request_hash = _hash_improved_data(improved_data)\n        if request_hash not in allowed_hashes:\n            logger.warning(\"Resume confirm rejected due to preview hash mismatch.\")\n            raise HTTPException(\n                status_code=400,\n                detail=\"Invalid improved resume data. Please retry preview.\",\n            )\n\n        stage = \"calculate_diff\"\n        response_warnings: list[str] = []\n        diff_summary, detailed_changes, diff_error = _calculate_diff_from_resume(\n            resume,\n            improved_data,\n        )","sourceCodeStart":1153,"sourceCodeEnd":1189,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/routers/resumes.py#L1153-L1189","documentation":"improve_resume_confirm_endpoint raises HTTP 400 when the job record has no preview_hashes (or the stored preview_hashes dict yields an empty allowed set), meaning no preview was ever run — or its hashes were never persisted — for this resume/job pair before confirmation was attempted.","triggerScenarios":"POST to improve/confirm where job.preview_hashes is missing, not a dict, or empty: confirming without ever calling preview, after a DB reset that wiped job metadata, or after preview failed/timed out so hashes were never stored.","commonSituations":"User bookmarks the confirm step and replays it later; preview timed out (error 124/125) but the client still tries confirm; backend redeploy wiped in-memory/DB preview state; preview was run for a different job_id.","solutions":["Run the preview endpoint first and wait for a successful response, then confirm immediately with the same resume_id/job_id pair","If preview previously timed out, raise REQUEST_TIMEOUT_SECONDS / NEXT_PUBLIC_REQUEST_TIMEOUT_MS and redo preview before confirming","After a DB reset or backend redeploy, restart the whole preview→confirm flow","Ensure the confirm request uses the same job_id that the successful preview used (hashes are stored per job)"],"exampleFix":"// before\nconfirm({resume_id, job_id}); // never previewed\n// after\nconst preview = await api.preview({resume_id, job_id}); // succeeds and stores hashes\nawait api.confirm({resume_id, job_id, improved_data: preview.improved});","handlingStrategy":"validation","validationCode":"async function canConfirm(resumeId: string, jobId: string): Promise<boolean> {\n  const job = await fetch(`/api/jobs/${jobId}`).then(r => r.json()).catch(() => null);\n  return !!job?.preview_hashes && Object.keys(job.preview_hashes).length > 0;\n}\nif (!(await canConfirm(resumeId, jobId))) await api.improvePreview({resume_id, job_id: jobId});","typeGuard":"function hasPreviewHashes(job: unknown): job is {preview_hashes: Record<string, string>} {\n  return typeof job === 'object' && job !== null &&\n    typeof (job as any).preview_hashes === 'object' && (job as any).preview_hashes !== null &&\n    Object.keys((job as any).preview_hashes).length > 0;\n}","tryCatchPattern":"try {\n  await api.improveConfirm({resume_id, job_id, improved_data});\n} catch (e) {\n  if (e.response?.status === 400 && /Preview required/.test(e.response.data?.detail ?? '')) {\n    const preview = await api.improvePreview({resume_id, job_id});\n    await api.improveConfirm({resume_id, job_id, improved_data: preview.improved});\n  } else throw e;\n}","preventionTips":["Only call confirm after a fully successful preview response — never after a timeout or failure","If preview timed out, fix the timeout config and rerun preview before confirming","Treat preview→confirm as one state machine; reset the flow after DB resets or redeploys","Use the same job_id for preview and confirm (hashes are persisted per job)"],"tags":["http-400","missing-preview","hash-validation","state-machine","resume-tailoring"],"backgroundTag":"missing-preview-before-confirm","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}