{"record":{"id":"df7c603fe70eace0","repo":"abi/screenshot-to-code","slug":"report-not-found","errorCode":null,"errorMessage":"Report not found","messagePattern":"Report not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"backend/routes/prompt_reports.py","lineNumber":158,"sourceCode":"        if os.path.isdir(run_logs_directory)\n        else 0\n    )\n\n    return PromptReportListResponse(\n        reports=reports,\n        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)","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/abi/screenshot-to-code/blob/d026163f586dfa8c5c10d28c36edd59a9d3b0e88/backend/routes/prompt_reports.py#L140-L176","documentation":"HTTPException(404, \"Report not found\") raised by GET /prompt-reports/content when the requested filename passes the PROMPT_REPORT_FILENAME_PATTERN check but os.path.isfile() finds no such file in the configured prompt-reports directory. The name is well-formed, but the file itself is absent by the time of the content request. Typical cause is a race with deletion (prune) or a reports-directory mismatch between listing and content endpoints.","triggerScenarios":"GET /prompt-reports/content?filename=<valid-shaped-name>.json where the file was just deleted by POST /prompt-reports/prune, was written to a different get_prompt_reports_directory() than the one being read, or the client uses a stale filename from an old list response.","commonSituations":"UI lists reports, a prune job or retention policy runs concurrently, and the user clicks a report that no longer exists; running multiple backend instances with different working directories so the relative reports dir resolves differently.","solutions":["Re-fetch the report list (the listing endpoint) and retry with a filename from the fresh response","Confirm the file actually exists: ls the directory returned by get_prompt_reports_directory() and compare with the requested filename (case-sensitive on Linux)","Check whether /prompt-reports/prune ran recently and deleted it; adjust max_age_days if retention is too aggressive","If running multiple instances, pin the reports directory to one absolute path via configuration so list and content endpoints agree"],"exampleFix":"// before (client)\nconst content = await fetch(`/prompt-reports/content?filename=${name}`); // 404 after prune\n\n// after\nlet resp = await fetch(`/prompt-reports/content?filename=${name}`);\nif (resp.status === 404) {\n  const { reports } = await fetch('/prompt-reports').then(r => r.json());\n  const fresh = reports.find(r => r.filename === name);\n  if (!fresh) throw new Error('Report was deleted');\n  resp = await fetch(`/prompt-reports/content?filename=${name}`);\n}","handlingStrategy":"validation","validationCode":"import httpx\n\ndef fetch_report_content(base: str, filename: str) -> dict:\n    listing = httpx.get(f\"{base}/prompt-reports\", timeout=10).json()\n    names = {r[\"filename\"] for r in listing.get(\"reports\", [])}\n    if filename not in names:\n        raise KeyError(f\"{filename} not in current report list; it may have been pruned\")\n    return httpx.get(\n        f\"{base}/prompt-reports/content\", params={\"filename\": filename}, timeout=10\n    ).json()","typeGuard":null,"tryCatchPattern":"try:\n    content = fetch(base, filename)\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 404:\n        listing = relist(base)  # refresh, then retry once with a fresh filename\n        ...","preventionTips":["Always pick filenames from a fresh list response immediately before fetching content","Treat reports as ephemeral: cache content, not filenames, if you need durability","Schedule prune jobs and UI refresh so they do not interleave with user reads"],"tags":["fastapi","http-404","file-system","race-condition"],"backgroundTag":null,"analyzedSha":"d026163f586dfa8c5c10d28c36edd59a9d3b0e88","analyzedAt":"2026-08-14T22:02:06.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}