jamiepine/voicebox · warning · HTTPException

offset must be >= 0

Error message

offset must be >= 0

What it means

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.

Solutions

  1. Clamp offset to max(0, computed) on the client.
  2. Disable the 'previous' control when already on the first page so offset never goes negative.
  3. Use 1-based page numbers and compute offset = (page-1)*limit after guarding page>=1.

Example fix

// before
fetch(`/captures?offset=${page * limit - limit}`) // page 0 -> negative
// after
const offset = Math.max(0, (page - 1) * limit);
fetch(`/captures?offset=${offset}`)
Defensive patterns

Strategy: validation

Validate before calling

const offset = Math.max(0, Number(reqOffset) || 0);
fetch(`/captures?offset=${offset}`);

Type guard

function isValidOffset(n: number): boolean {
  return Number.isInteger(n) && n >= 0;
}

Prevention

When it happens

Trigger: 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.

Common situations: Pagination 'previous' button on page 1 computing offset = -limit; a script decrementing offset below zero.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/77be34ca3dc16f90. Report an issue: GitHub.

Appendix: source

Thrown at backend/routes/captures.py:79

        raise HTTPException(status_code=500, detail=str(e))

    return models.CaptureCreateResponse(
        **capture.model_dump(),
        auto_refine=bool(saved.auto_refine),
        allow_auto_paste=bool(saved.allow_auto_paste),
    )


@router.get("/captures", response_model=models.CaptureListResponse)
async def list_captures_endpoint(
    limit: int = 50,
    offset: int = 0,
    db: Session = Depends(get_db),
):
    if limit < 1 or limit > 200:
        raise HTTPException(status_code=400, detail="limit must be between 1 and 200")
    if offset < 0:
        raise HTTPException(status_code=400, detail="offset must be >= 0")

    items, total = captures_service.list_captures(db, limit=limit, offset=offset)
    return models.CaptureListResponse(items=items, total=total)


@router.get("/captures/{capture_id}", response_model=models.CaptureResponse)
async def get_capture_endpoint(capture_id: str, db: Session = Depends(get_db)):
    capture = captures_service.get_capture(capture_id, db)
    if not capture:
        raise HTTPException(status_code=404, detail="Capture not found")
    return capture


@router.get("/captures/{capture_id}/audio")
async def get_capture_audio_endpoint(capture_id: str, db: Session = Depends(get_db)):
    """Stream the original capture audio file."""
    row = db.query(DBCapture).filter(DBCapture.id == capture_id).first()
    if not row:

View on GitHub (pinned to 51f49dea19)