From 3c608046e21bd88ba4fd3da7cdd8ff802fea6139 Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Tue, 26 May 2026 16:05:47 +0300 Subject: [PATCH] Bound ?limit and ?offset on /jobs and /dlq --- .../haiku/rag/ingester/api/routes/dlq.py | 6 +++--- .../haiku/rag/ingester/api/routes/jobs.py | 6 +++--- tests/ingester/test_api.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/ingester/api/routes/dlq.py b/haiku_rag_slim/haiku/rag/ingester/api/routes/dlq.py index 9cef98e0..c9a9d733 100644 --- a/haiku_rag_slim/haiku/rag/ingester/api/routes/dlq.py +++ b/haiku_rag_slim/haiku/rag/ingester/api/routes/dlq.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from haiku.rag.ingester.api.server import APIState, get_state from haiku.rag.ingester.queue.models import Job, JobStatus @@ -9,8 +9,8 @@ router = APIRouter(prefix="/dlq", tags=["dlq"]) @router.get("", response_model=list[Job]) async def list_dlq( source_id: str | None = None, - limit: int = 50, - offset: int = 0, + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0), state: APIState = Depends(get_state), ) -> list[Job]: """Jobs that exhausted retries or hit a permanent error.""" diff --git a/haiku_rag_slim/haiku/rag/ingester/api/routes/jobs.py b/haiku_rag_slim/haiku/rag/ingester/api/routes/jobs.py index 7db80b58..0c67d5da 100644 --- a/haiku_rag_slim/haiku/rag/ingester/api/routes/jobs.py +++ b/haiku_rag_slim/haiku/rag/ingester/api/routes/jobs.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from haiku.rag.ingester.api.schemas import CancelResponse from haiku.rag.ingester.api.server import APIState, get_state @@ -12,8 +12,8 @@ async def list_jobs( status: JobStatus | None = None, source_id: str | None = None, uri: str | None = None, - limit: int = 50, - offset: int = 0, + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0), state: APIState = Depends(get_state), ) -> list[Job]: return await state.job_repo.list_jobs( diff --git a/tests/ingester/test_api.py b/tests/ingester/test_api.py index 949030d0..dd9e107b 100644 --- a/tests/ingester/test_api.py +++ b/tests/ingester/test_api.py @@ -195,6 +195,20 @@ async def test_list_jobs_returns_recent_first(state, jobs): assert [j["id"] for j in payload] == [j2.id, j1.id] +@pytest.mark.asyncio +async def test_list_jobs_rejects_out_of_range_limit_and_offset(state): + """Limit is capped at 500 and >=1; offset is >=0. Without bounds a + malicious or careless ?limit=10000000 would block the event loop on + serialization.""" + async with _client(state) as client: + for q in ("/jobs?limit=0", "/jobs?limit=501", "/jobs?offset=-1"): + resp = await client.get(q) + assert resp.status_code == 422, q + for q in ("/dlq?limit=0", "/dlq?limit=501", "/dlq?offset=-1"): + resp = await client.get(q) + assert resp.status_code == 422, q + + @pytest.mark.asyncio async def test_list_jobs_filters_by_source_and_status(state, jobs): # Enqueue b first so claim_next reaches it before the a row.