{"record":{"id":"77be34ca3dc16f90","repo":"jamiepine/voicebox","slug":"offset-must-be-0-77be34","errorCode":null,"errorMessage":"offset must be >= 0","messagePattern":"offset must be >= 0","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"backend/routes/captures.py","lineNumber":79,"sourceCode":"        raise HTTPException(status_code=500, detail=str(e))\n\n    return models.CaptureCreateResponse(\n        **capture.model_dump(),\n        auto_refine=bool(saved.auto_refine),\n        allow_auto_paste=bool(saved.allow_auto_paste),\n    )\n\n\n@router.get(\"/captures\", response_model=models.CaptureListResponse)\nasync def list_captures_endpoint(\n    limit: int = 50,\n    offset: int = 0,\n    db: Session = Depends(get_db),\n):\n    if limit < 1 or limit > 200:\n        raise HTTPException(status_code=400, detail=\"limit must be between 1 and 200\")\n    if offset < 0:\n        raise HTTPException(status_code=400, detail=\"offset must be >= 0\")\n\n    items, total = captures_service.list_captures(db, limit=limit, offset=offset)\n    return models.CaptureListResponse(items=items, total=total)\n\n\n@router.get(\"/captures/{capture_id}\", response_model=models.CaptureResponse)\nasync def get_capture_endpoint(capture_id: str, db: Session = Depends(get_db)):\n    capture = captures_service.get_capture(capture_id, db)\n    if not capture:\n        raise HTTPException(status_code=404, detail=\"Capture not found\")\n    return capture\n\n\n@router.get(\"/captures/{capture_id}/audio\")\nasync def get_capture_audio_endpoint(capture_id: str, db: Session = Depends(get_db)):\n    \"\"\"Stream the original capture audio file.\"\"\"\n    row = db.query(DBCapture).filter(DBCapture.id == capture_id).first()\n    if not row:","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/captures.py#L61-L97","documentation":"Returned as a 400 from GET /captures when the `offset` query param is negative. Offset must be >= 0 for SQL LIMIT/OFFSET to be valid; a negative offset would either error in the DB layer or behave unpredictably. The route validates this explicitly before calling list_captures.","triggerScenarios":"GET /captures?offset=-1 or any negative integer for offset. Triggered by a client computing offset as (page-1)*limit with page<=0, or a decrement-past-zero bug in a 'previous page' control.","commonSituations":"Pagination 'previous' button on page 1 computing offset = -limit; a script decrementing offset below zero.","solutions":["Clamp offset to max(0, computed) on the client.","Disable the 'previous' control when already on the first page so offset never goes negative.","Use 1-based page numbers and compute offset = (page-1)*limit after guarding page>=1."],"exampleFix":"// before\nfetch(`/captures?offset=${page * limit - limit}`) // page 0 -> negative\n// after\nconst offset = Math.max(0, (page - 1) * limit);\nfetch(`/captures?offset=${offset}`)","handlingStrategy":"validation","validationCode":"const offset = Math.max(0, Number(reqOffset) || 0);\nfetch(`/captures?offset=${offset}`);","typeGuard":"function isValidOffset(n: number): boolean {\n  return Number.isInteger(n) && n >= 0;\n}","tryCatchPattern":null,"preventionTips":["Compute offset as max(0, (page-1)*limit) with page guarded to >= 1.","Disable the 'previous' control on the first page.","Centralize pagination math in one helper to avoid ad-hoc negative offsets."],"tags":["capture","pagination","validation","query-param","fastapi"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}