{"record":{"id":"b5dcd9397abd0501","repo":"abi/screenshot-to-code","slug":"max-age-days-must-be-1-b5dcd9","errorCode":null,"errorMessage":"max_age_days must be >= 1","messagePattern":"max_age_days must be >= 1","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"backend/routes/prompt_reports.py","lineNumber":172,"sourceCode":"        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)\n\n    cutoff = datetime.now() - timedelta(days=request.max_age_days)\n    cutoff_timestamp = cutoff.timestamp()\n\n    deleted_count = 0\n    freed_bytes = 0\n    for entry in os.scandir(run_logs_directory):\n        # agent_runs has its own index + prune endpoint (/agent-runs/prune);\n        # rmtree'ing it here would destroy the SQLite index and fresh runs.\n        if entry.is_dir() and entry.name == \"agent_runs\":\n            continue\n\n        # The prompt_reports subdirectory itself is permanent; prune inside it.\n        if entry.is_dir() and entry.name == \"prompt_reports\":","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/abi/screenshot-to-code/blob/d026163f586dfa8c5c10d28c36edd59a9d3b0e88/backend/routes/prompt_reports.py#L154-L190","documentation":"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.","triggerScenarios":"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.","commonSituations":"Frontend form sending 0 for an unset retention field; scripts written against an older API that allowed 0; copy-pasted curl payloads.","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"],"exampleFix":"# before\nclass PrunePromptReportsRequest(BaseModel):\n    max_age_days: int\n\n# after\nclass PrunePromptReportsRequest(BaseModel):\n    max_age_days: int = Field(ge=1, le=3650)","handlingStrategy":"validation","validationCode":"def prune_payload(max_age_days: int | None) -> dict:\n    days = max_age_days if max_age_days else 30  # never send 0 for 'unset'\n    if days < 1:\n        raise ValueError(f\"max_age_days must be >= 1, got {days}\")\n    return {\"max_age_days\": days}","typeGuard":"def is_valid_prune_request(payload: dict) -> bool:\n    v = payload.get(\"max_age_days\")\n    return isinstance(v, int) and not isinstance(v, bool) and v >= 1","tryCatchPattern":"try:\n    client.post(f\"{base}/prompt-reports/prune\", json=prune_payload(days))\nexcept HTTPStatusError as e:\n    if e.response.status_code == 400:\n        fix_and_notify(\"max_age_days must be >= 1\")  # client bug, do not retry","preventionTips":["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"],"tags":["fastapi","validation","http-400","pydantic"],"backgroundTag":null,"analyzedSha":"d026163f586dfa8c5c10d28c36edd59a9d3b0e88","analyzedAt":"2026-08-14T22:02:06.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}