odysseus-dev/odysseus · error · HTTPException

{key} must be an integer

Error message

{key} must be an integer

What it means

Raised as HTTP 400 by the settings-update endpoint when a request body key that is listed in _INT_RANGES cannot be coerced to int with int(val). The endpoint iterates DEFAULT_SETTINGS, so any numeric setting (e.g. a retry count or threshold) sent as a string like "abc", null, or a nested object triggers this. int() also rejects floats-with-junk ("1.5" raises ValueError) and None (TypeError).

Source

Thrown at routes/auth_routes.py:724

        current = _load_settings()
        # Per-key validation for numeric settings: coerce to int and clamp to a
        # sane range so a bad value can't disable the agent or let it run away.
        _INT_RANGES = {
            "agent_max_rounds": (1, 200),
            "agent_max_tool_calls": (0, 1000),  # 0 = unlimited
        }
        for key in DEFAULT_SETTINGS:
            if key in RETIRED_SETTING_KEYS:
                continue
            if key not in body:
                continue
            val = body[key]
            if key in _INT_RANGES:
                lo, hi = _INT_RANGES[key]
                try:
                    val = int(val)
                except (TypeError, ValueError):
                    raise HTTPException(400, f"{key} must be an integer")
                val = max(lo, min(val, hi))
            current[key] = val
        _save_settings(current)
        return without_retired_settings(current)

    # ---- Integrations CRUD ----

    # Run migration on startup
    migrate_from_settings()

    @router.get("/integrations")
    async def list_integrations_route(request: Request):
        """List all integrations (admin only, keys masked)."""
        user = _get_current_user(request)
        if not user or not auth_manager.is_admin(user):
            raise HTTPException(403, "Admin only")
        items = load_integrations()
        # Mask API keys for frontend display

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Coerce to integer on the client before sending: Math.trunc(Number(value)) and skip the key when the result is NaN.
  2. If fractional input is legitimate, send Math.round(value) or extend _INT_RANGES handling to accept floats.
  3. Check the key name in the 400 message — it names the exact setting that failed validation.
  4. Note the endpoint clamps to [lo, hi] after conversion, so only the type, not the range, matters here.

Example fix

// before
await fetch('/api/settings', {method:'PUT', body: JSON.stringify({max_retries: retryInput.value})});

// after
const n = Math.trunc(Number(retryInput.value));
if (!Number.isFinite(n)) throw new Error('max_retries must be an integer');
await fetch('/api/settings', {method:'PUT', body: JSON.stringify({max_retries: n})});
Defensive patterns

Strategy: validation

Validate before calling

const INT_SETTINGS = new Set(['max_retries', 'session_timeout', /* keys from _INT_RANGES */]);
function sanitizeSettings(body) {
  for (const [k, v] of Object.entries(body)) {
    if (INT_SETTINGS.has(k)) {
      const n = Math.trunc(Number(v));
      if (!Number.isFinite(n)) throw new Error(`${k} must be an integer`);
      body[k] = n;
    }
  }
  return body;
}

Type guard

const isIntLike = (v: unknown): v is number =>
  (typeof v === 'number' && Number.isInteger(v)) ||
  (typeof v === 'string' && /^-?\d+$/.test(v.trim()));

Try / catch

try { await putSettings(body); } catch (e) {
  if (e.status === 400 && /must be an integer/.test(e.message)) { /* flag the named field in the form */ }
  else throw e;
}

Prevention

When it happens

Trigger: PUT/PATCH the settings route with body {"<int-setting-key>": "twelve"}, {"<int-setting-key>": null}, or {"<int-setting-key>": {}}. Passing "1.5" (non-integer string) also raises. Keys not in _INT_RANGES or absent from body are skipped, so only ranged integer settings hit this path.

Common situations: Frontend form sending free-text input without Number() coercion; JSON null for an optional numeric field; a boolean or array serialized where an int was expected; locale-formatted numbers like "1 000".

Related errors


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