abi/screenshot-to-code · warning · HTTPException

Invalid report filename

Error message

Invalid report filename

What it means

400 raised by GET /prompt-reports/content in backend/routes/prompt_reports.py:154 when the filename query parameter does not match PROMPT_REPORT_FILENAME_PATTERN — the strict schema ^prompt_report_<YYYYMMDD>_<HHMMSS>_<8 hex session>_t<turn>_<provider>_<model>.json$ (backend/fs_logging/prompt_reports.py:29). This is also a path-traversal guard: any ../ or unexpected character fails the match before the file is opened.

Source

Thrown at backend/routes/prompt_reports.py:154

    reports.sort(key=lambda report: (report.created_at, report.turn), reverse=True)

    total_size_bytes = (
        _directory_size_bytes(run_logs_directory)
        if os.path.isdir(run_logs_directory)
        else 0
    )

    return PromptReportListResponse(
        reports=reports,
        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")

View on GitHub (pinned to d026163f58)

Solutions

  1. Use the exact filename returned by the GET /prompt-reports listing endpoint (its summaries come from the same pattern).
  2. URL-encode the filename when building the query string.
  3. If the file legitimately exists but is named differently, it was not written by this logger — check the naming pattern before renaming to match.

Example fix

// before
const url = `/prompt-reports/content?filename=${reportDate}_${reportTime}.json`;

// after
const url = `/prompt-reports/content?filename=${encodeURIComponent(report.filename)}`; // exact value from GET /prompt-reports
Defensive patterns

Strategy: validation

Validate before calling

const FILENAME_RE = /^prompt_report_\d{8}_\d{6}_[0-9a-f]{8}_t\d+_[a-z0-9]+_[A-Za-z0-9.-]+\.json$/;
if (!FILENAME_RE.test(filename)) throw new Error(`Invalid report filename: ${filename}`);

Type guard

function isPromptReportFilename(name: string): boolean {
  return /^prompt_report_\d{8}_\d{6}_[0-9a-f]{8}_t\d+_[a-z0-9]+_[A-Za-z0-9.-]+\.json$/.test(name);
}

Try / catch

const res = await fetch(`/prompt-reports/content?filename=${encodeURIComponent(filename)}`);
if (res.status === 400) { /* do not retry: name is malformed; re-fetch from list */ }
if (res.status === 404) { /* report was pruned; refresh the list */ }

Prevention

When it happens

Trigger: Calling the endpoint with a hand-built filename, a filename copied with whitespace/case changes, a traversal attempt like ../../backend/.env, or a report written by an older naming scheme.

Common situations: Clients constructing the URL from report metadata fields instead of using the exact filename from the reports list; reports migrated from another machine with different naming; security scanners probing with traversal strings.

Related errors


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