fix(api): validate pagination bounds on run-list endpoints (#553) (#554)

* fix(api): validate pagination bounds on run-list endpoints (#553)

GET /workflow/{id}/runs and GET /campaign/{id}/runs declared bare
`page: int = 1` / `limit: int = 50` params, then computed
`total_pages = (total_count + limit - 1) // limit`. A `?limit=0` raised an
unhandled ZeroDivisionError (HTTP 500), and negative limit/page produced a
negative offset and nonsensical pagination.

Add `Query(ge=1, le=100)` / `Query(1, ge=1)` bounds to both endpoints,
matching the sibling list endpoints (/usage/runs and the superuser runs
endpoint) that already validate these. Out-of-range values now return 422.

Adds a regression test covering limit=0/-5/101 and page=0 on both endpoints.

Fixes #553

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(docs): regenerate openapi.json for run-list pagination bounds (#553)

The added Query(ge/le) bounds on the workflow-run and campaign-run list
endpoints changed the OpenAPI schema; regenerate the committed spec via
`python -m scripts.dump_docs_openapi` so the drift-check passes. Only the
limit/page parameter schemas for those two endpoints change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
Amaan Javed 2026-07-18 03:59:28 -07:00 committed by GitHub
parent ade0ee9104
commit 9471041b8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 45 additions and 5 deletions

View file

@ -696,8 +696,8 @@ async def update_campaign(
@router.get("/{campaign_id}/runs")
async def get_campaign_runs(
campaign_id: int,
page: int = 1,
limit: int = 50,
page: int = Query(1, ge=1, description="Page number (starts from 1)"),
limit: int = Query(50, ge=1, le=100, description="Number of items per page"),
filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"),
sort_by: Optional[str] = Query(
None, description="Field to sort by (e.g., 'duration', 'created_at')"

View file

@ -1392,8 +1392,8 @@ class WorkflowRunsResponse(BaseModel):
@router.get("/{workflow_id}/runs")
async def get_workflow_runs(
workflow_id: int,
page: int = 1,
limit: int = 50,
page: int = Query(1, ge=1, description="Page number (starts from 1)"),
limit: int = Query(50, ge=1, le=100, description="Number of items per page"),
filters: Optional[str] = Query(None, description="JSON-encoded filter criteria"),
sort_by: Optional[str] = Query(
None, description="Field to sort by (e.g., 'duration', 'created_at')"