{"record":{"id":"a1e9b84b8e13635c","repo":"srbhr/Resume-Matcher","slug":"invalid-improved-resume-data-please-retry-preview","errorCode":null,"errorMessage":"Invalid improved resume data. Please retry preview.","messagePattern":"Invalid improved resume data\\. Please retry preview\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"apps/backend/app/routers/resumes.py","lineNumber":1149,"sourceCode":"\n    feature_config = _load_config()\n    enable_cover_letter = feature_config.get(\"enable_cover_letter\", False)\n    enable_outreach = feature_config.get(\"enable_outreach_message\", False)\n    enable_interview_prep = feature_config.get(\"enable_interview_prep\", False)\n    language = get_content_language()\n\n    stage = \"serialize_improved_data\"\n    detail = \"Failed to confirm resume. Please try again.\"\n    try:\n        improved_data = request.improved_data.model_dump()\n        improved_text = json.dumps(improved_data, indent=2)\n        # NOTE: This endpoint relies on preview-hash validation to ensure the payload matches a prior preview.\n        # Stronger guarantees would require server-side preview storage or re-running the improvement.\n        try:\n            _validate_confirm_payload(_get_original_resume_data(resume), improved_data)\n        except ValueError as e:\n            logger.warning(\"Resume confirm rejected: %s\", e)\n            raise HTTPException(\n                status_code=400,\n                detail=\"Invalid improved resume data. Please retry preview.\",\n            )\n        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(","sourceCodeStart":1131,"sourceCodeEnd":1167,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/routers/resumes.py#L1131-L1167","documentation":"improve_resume_confirm_endpoint raises HTTP 400 when _validate_confirm_payload (comparing the improved payload against the original resume data) raises ValueError, meaning the client sent improved data inconsistent with the stored original or malformed relative to what preview produced.","triggerScenarios":"POST to improve/confirm whose improved_data fails payload validation — edited/tampered sections between preview and confirm, a changed schema, or confirming with hand-built JSON instead of the exact preview response body.","commonSituations":"Frontend mutated the preview response (e.g. injected a new section or rewrote IDs) before confirming; backend was upgraded mid-flow so validation rules differ; client retries confirm with an old payload against a newer backend.","solutions":["Re-run the preview and confirm with the preview response's improved data unmodified (round-trip the exact object)","Clear stale client state (cached preview payload) and start the tailoring flow again","Check backend version mismatch: if the server was updated between preview and confirm, redo preview","Inspect backend logs ('Resume confirm rejected: ...') to see which validation rule failed"],"exampleFix":"// before\nconst edited = {...previewData.improved};\nedited.sections.push(newSection); // tampered\nconfirm({resume_id, job_id, improved_data: edited});\n// after\nconfirm({resume_id, job_id, improved_data: previewData.improved}); // exact payload","handlingStrategy":"validation","validationCode":"function isUntouchedPreview(preview: {improved: object}, toSend: object): boolean {\n  // send the exact preview payload — deep-equal check before confirm\n  return JSON.stringify(preview.improved) === JSON.stringify(toSend);\n}\nif (!isUntouchedPreview(preview, payload.improved_data)) payload.improved_data = preview.improved;","typeGuard":"function looksLikeImprovedResume(d: unknown): d is Record<string, unknown> {\n  return typeof d === 'object' && d !== null &&\n    Array.isArray((d as any).sections) && (d as any).sections.length > 0 &&\n    typeof (d as any).contact === 'object';\n}","tryCatchPattern":"try {\n  await api.improveConfirm({resume_id, job_id, improved_data});\n} catch (e) {\n  if (e.response?.status === 400 && /Invalid improved resume data/.test(e.response.data?.detail ?? '')) {\n    const fresh = await api.improvePreview({resume_id, job_id});\n    await api.improveConfirm({resume_id, job_id, improved_data: fresh.improved});\n  } else throw e;\n}","preventionTips":["Never mutate the preview response before sending it back — round-trip the exact object","Avoid hand-building confirm payloads; derive them from the preview response","Restart preview→confirm after any backend upgrade between the two calls","Read the 'Resume confirm rejected' warning in backend logs to learn which rule failed"],"tags":["http-400","payload-validation","state-mismatch","resume-tailoring"],"backgroundTag":"payload-validation-failed","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}