abi/screenshot-to-code · warning · HTTPException

max_age_days must be >= 1

Error message

max_age_days must be >= 1

What it means

HTTPException(400, "max_age_days must be >= 1") raised by POST /prompt-reports/prune when the request body's max_age_days is 0 or negative. The Pydantic request model declares the field as a plain int, so type-valid but out-of-range values reach the handler, which enforces the lower bound manually. It is purely a client payload validation error.

Source

Thrown at backend/routes/prompt_reports.py:172

        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
    for entry in os.scandir(run_logs_directory):
        # agent_runs has its own index + prune endpoint (/agent-runs/prune);
        # rmtree'ing it here would destroy the SQLite index and fresh runs.
        if entry.is_dir() and entry.name == "agent_runs":
            continue

        # The prompt_reports subdirectory itself is permanent; prune inside it.
        if entry.is_dir() and entry.name == "prompt_reports":

View on GitHub (pinned to d026163f58)

Solutions

  1. Send a value >= 1, e.g. {"max_age_days": 30} — 0 is rejected because a cutoff of 'now' would delete everything and negatives are meaningless
  2. Fix the client so an empty retention field maps to a sensible default (e.g. 30) instead of 0
  3. Optionally move the constraint into the Pydantic model so FastAPI returns a standard 422 before the handler runs

Example fix

# before
class PrunePromptReportsRequest(BaseModel):
    max_age_days: int

# after
class PrunePromptReportsRequest(BaseModel):
    max_age_days: int = Field(ge=1, le=3650)
Defensive patterns

Strategy: validation

Validate before calling

def prune_payload(max_age_days: int | None) -> dict:
    days = max_age_days if max_age_days else 30  # never send 0 for 'unset'
    if days < 1:
        raise ValueError(f"max_age_days must be >= 1, got {days}")
    return {"max_age_days": days}

Type guard

def is_valid_prune_request(payload: dict) -> bool:
    v = payload.get("max_age_days")
    return isinstance(v, int) and not isinstance(v, bool) and v >= 1

Try / catch

try:
    client.post(f"{base}/prompt-reports/prune", json=prune_payload(days))
except HTTPStatusError as e:
    if e.response.status_code == 400:
        fix_and_notify("max_age_days must be >= 1")  # client bug, do not retry

Prevention

When it happens

Trigger: POST /prompt-reports/prune with {"max_age_days": 0} or {"max_age_days": -5}; a client that defaults max_age_days to 0 when the user leaves the field empty.

Common situations: Frontend form sending 0 for an unset retention field; scripts written against an older API that allowed 0; copy-pasted curl payloads.

Related errors


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