abi/screenshot-to-code · error · HTTPException

Failed to read report: {e}

Error message

Failed to read report: {e}

What it means

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.

Source

Thrown at backend/routes/prompt_reports.py:164

        total_size_bytes=total_size_bytes,
        reports_directory=reports_directory,
    )


@router.get("/prompt-reports/content")
async def get_prompt_report_content(filename: str) -> Any:
    if PROMPT_REPORT_FILENAME_PATTERN.match(filename) is None:
        raise HTTPException(status_code=400, detail="Invalid report filename")

    filepath = os.path.join(get_prompt_reports_directory(), filename)
    if not os.path.isfile(filepath):
        raise HTTPException(status_code=404, detail="Report not found")

    try:
        with open(filepath, "r", encoding="utf-8") as f:
            return json.load(f)
    except (OSError, json.JSONDecodeError) as e:
        raise HTTPException(status_code=500, detail=f"Failed to read report: {e}")


@router.post("/prompt-reports/prune", response_model=PrunePromptReportsResponse)
async def prune_prompt_reports(
    request: PrunePromptReportsRequest,
) -> PrunePromptReportsResponse:
    if request.max_age_days < 1:
        raise HTTPException(status_code=400, detail="max_age_days must be >= 1")

    run_logs_directory = get_run_logs_directory()
    if not os.path.isdir(run_logs_directory):
        return PrunePromptReportsResponse(deleted_count=0, freed_bytes=0)

    cutoff = datetime.now() - timedelta(days=request.max_age_days)
    cutoff_timestamp = cutoff.timestamp()

    deleted_count = 0
    freed_bytes = 0

View on GitHub (pinned to d026163f58)

Solutions

  1. Validate the file out-of-band: python -m json.tool <file> to confirm whether it is truncated/corrupt
  2. Check permissions/ownership on the file and the reports directory (chmod/chown) if the OSError path is taken
  3. Regenerate the report (re-run the generation that produced it) to replace the corrupt file
  4. 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

Example fix

# before (writer)
with open(target, "w", encoding="utf-8") as f:
    json.dump(report, f)  # reader can see truncated JSON

# after (writer)
import tempfile, os
fd, tmp = tempfile.mkstemp(dir=target.parent, suffix=".tmp")
try:
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        json.dump(report, f)
    os.replace(tmp, target)
except BaseException:
    os.unlink(tmp); raise
Defensive patterns

Strategy: try-catch

Validate before calling

import json, os

def safe_report_content(filepath: str) -> dict:
    if not os.access(filepath, os.R_OK):
        raise PermissionError(filepath)
    with open(filepath, encoding="utf-8") as f:
        return json.load(f)  # JSONDecodeError still possible for partial writes

Try / catch

try:
    content = client.get(f"{base}/prompt-reports/content", params={"filename": fn}).json()
except (OSError, json.JSONDecodeError):
    regenerate_report(fn)  # or mark as corrupt and skip; do not retry the same file blindly

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of abi/screenshot-to-code@d026163f58 (2026-08-14). Data as JSON: /api/errors/dae081575cb41fa1. Report an issue: GitHub.