{"record":{"id":"dae081575cb41fa1","repo":"abi/screenshot-to-code","slug":"failed-to-read-report-e","errorCode":null,"errorMessage":"Failed to read report: {e}","messagePattern":"Failed to read report: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"backend/routes/prompt_reports.py","lineNumber":164,"sourceCode":"        total_size_bytes=total_size_bytes,\n        reports_directory=reports_directory,\n    )\n\n\n@router.get(\"/prompt-reports/content\")\nasync def get_prompt_report_content(filename: str) -> Any:\n    if PROMPT_REPORT_FILENAME_PATTERN.match(filename) is None:\n        raise HTTPException(status_code=400, detail=\"Invalid report filename\")\n\n    filepath = os.path.join(get_prompt_reports_directory(), filename)\n    if not os.path.isfile(filepath):\n        raise HTTPException(status_code=404, detail=\"Report not found\")\n\n    try:\n        with open(filepath, \"r\", encoding=\"utf-8\") as f:\n            return json.load(f)\n    except (OSError, json.JSONDecodeError) as e:\n        raise HTTPException(status_code=500, detail=f\"Failed to read report: {e}\")\n\n\n@router.post(\"/prompt-reports/prune\", response_model=PrunePromptReportsResponse)\nasync def prune_prompt_reports(\n    request: PrunePromptReportsRequest,\n) -> PrunePromptReportsResponse:\n    if request.max_age_days < 1:\n        raise HTTPException(status_code=400, detail=\"max_age_days must be >= 1\")\n\n    run_logs_directory = get_run_logs_directory()\n    if not os.path.isdir(run_logs_directory):\n        return PrunePromptReportsResponse(deleted_count=0, freed_bytes=0)\n\n    cutoff = datetime.now() - timedelta(days=request.max_age_days)\n    cutoff_timestamp = cutoff.timestamp()\n\n    deleted_count = 0\n    freed_bytes = 0","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/abi/screenshot-to-code/blob/d026163f586dfa8c5c10d28c36edd59a9d3b0e88/backend/routes/prompt_reports.py#L146-L182","documentation":"HTTPException(500, \"Failed to read report: ...\") raised when open(filepath) or json.load(f) raises OSError or json.JSONDecodeError after the isfile() check already passed. It means the file exists but is unreadable (permissions changed, deleted mid-read) or its contents are not valid JSON — commonly a truncated/partially-written report being read while the writer is still writing it.","triggerScenarios":"GET /prompt-reports/content on a report file that is being written concurrently (no atomic write, so a reader sees truncated JSON); file with restrictive permissions (OSError EACCES); empty file left by a crashed writer process.","commonSituations":"Report writer and reader share a directory without write-atomicity; process crashed mid-write leaving a half-written JSON file; files copied between users/containers losing read permissions.","solutions":["Validate the file out-of-band: python -m json.tool <file> to confirm whether it is truncated/corrupt","Check permissions/ownership on the file and the reports directory (chmod/chown) if the OSError path is taken","Regenerate the report (re-run the generation that produced it) to replace the corrupt file","Fix the writer to write atomically: write to a temp file in the same directory, then os.replace() onto the final name, so readers never see partial content"],"exampleFix":"# before (writer)\nwith open(target, \"w\", encoding=\"utf-8\") as f:\n    json.dump(report, f)  # reader can see truncated JSON\n\n# after (writer)\nimport tempfile, os\nfd, tmp = tempfile.mkstemp(dir=target.parent, suffix=\".tmp\")\ntry:\n    with os.fdopen(fd, \"w\", encoding=\"utf-8\") as f:\n        json.dump(report, f)\n    os.replace(tmp, target)\nexcept BaseException:\n    os.unlink(tmp); raise","handlingStrategy":"try-catch","validationCode":"import json, os\n\ndef safe_report_content(filepath: str) -> dict:\n    if not os.access(filepath, os.R_OK):\n        raise PermissionError(filepath)\n    with open(filepath, encoding=\"utf-8\") as f:\n        return json.load(f)  # JSONDecodeError still possible for partial writes","typeGuard":null,"tryCatchPattern":"try:\n    content = client.get(f\"{base}/prompt-reports/content\", params={\"filename\": fn}).json()\nexcept (OSError, json.JSONDecodeError):\n    regenerate_report(fn)  # or mark as corrupt and skip; do not retry the same file blindly","preventionTips":["Write reports atomically (temp file + os.replace) so readers never see partial JSON","Monitor for zero-byte or truncated report files after writer crashes","Keep report directory ownership consistent across writer and reader processes"],"tags":["fastapi","json","file-system","atomic-write"],"backgroundTag":null,"analyzedSha":"d026163f586dfa8c5c10d28c36edd59a9d3b0e88","analyzedAt":"2026-08-14T22:02:06.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}