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
- 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
- Fix the client so an empty retention field maps to a sensible default (e.g. 30) instead of 0
- 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
- Model the request with Pydantic Field(ge=1) client-side so invalid values never leave the app
- Map an empty retention UI field to an explicit default, not 0
- Remember 0 means 'delete everything older than now' semantically — the API refuses it on purpose
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
- Invalid run id
- max_age_days must be >= 1
- Design system name is required
- Invalid eval set name: {set_name!r}
- Not a set image: {filename!r}
AI-assisted analysis of abi/screenshot-to-code@d026163f58 (2026-08-14).
Data as JSON: /api/errors/b5dcd9397abd0501.
Report an issue: GitHub.