odysseus-dev/odysseus · warning · HTTPException

Invalid offset

Error message

Invalid offset

What it means

Raised as HTTP 400 by _clamp_pagination when the offset query parameter cannot be parsed as an int — int() raises TypeError/ValueError on non-numeric strings, lists (repeated query params), dicts, or unhashable junk. Empty/None are treated as 0, so this fires only on genuinely malformed input.

Source

Thrown at routes/codex_routes.py:140

    if not getattr(request.state, "api_token", False):
        require_admin(request)
    return owner


def _find_endpoint(router: APIRouter | None, method: str, path: str):
    if router is None:
        return None
    for route in getattr(router, "routes", []):
        if getattr(route, "path", "") == path and method in getattr(route, "methods", set()):
            return route.endpoint
    return None


def _clamp_pagination(offset: Any, limit: Any, *, default_limit: int = 50, max_limit: int = 50) -> tuple[int, int]:
    try:
        parsed_offset = int(0 if offset in (None, "") else offset)
    except (TypeError, ValueError):
        raise HTTPException(400, "Invalid offset")
    try:
        parsed_limit = int(default_limit if limit in (None, "") else limit)
    except (TypeError, ValueError):
        raise HTTPException(400, "Invalid limit")
    return max(0, parsed_offset), max(1, min(parsed_limit, max_limit))


def setup_codex_routes(
    email_router: APIRouter | None = None,
    memory_router: APIRouter | None = None,
    calendar_router: APIRouter | None = None,
    document_router: APIRouter | None = None,
) -> APIRouter:
    router = APIRouter(prefix="/api/codex", tags=["codex"])
    email_list_endpoint = _find_endpoint(email_router, "GET", "/api/email/list")
    email_read_endpoint = _find_endpoint(email_router, "GET", "/api/email/read/{uid}")
    email_send_endpoint = _find_endpoint(email_router, "POST", "/api/email/send")
    email_draft_endpoint = _find_endpoint(email_router, "POST", "/api/email/draft")

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Send an integer offset, or omit the parameter entirely (defaults to 0).
  2. In the client, coerce with Number.isInteger(Number(offset)) before building the URL.
  3. Avoid sending the parameter twice; FastAPI turns repeated query keys into a list which fails int().

Example fix

// before
fetch(`/api/codex/todos?offset=${page}`)  // page === undefined

// after
const offset = Number.isInteger(page) ? page : 0;
fetch(`/api/codex/todos?offset=${offset}`)
Defensive patterns

Strategy: validation

Validate before calling

function safeOffset(v: unknown): number {
  const n = Number(v);
  return Number.isInteger(n) && n >= 0 ? n : 0;
}
const url = `/api/codex/todos?offset=${safeOffset(rawOffset)}`;

Type guard

function isPaginationInt(v: unknown): v is number {
  return typeof v === 'number' && Number.isInteger(v) && v >= 0;
}

Try / catch

try { r = await get(url) } catch (e) { if (e.status === 400 && e.detail === 'Invalid offset') { retry with offset=0 } else throw }

Prevention

When it happens

Trigger: GET /api/codex/...?offset=abc, ?offset=1.5, ?offset=5&offset=10 (FastAPI yields a list), or a client serializing null as the string 'null'.

Common situations: Frontend interpolating an undefined variable into the query string ('offset=undefined'); passing a float page math result; copy-pasted URLs with stray characters.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/f3505af02aa216e6. Report an issue: GitHub.