{"record":{"id":"df8850dde167e77b","repo":"odysseus-dev/odysseus","slug":"key-must-be-an-integer","errorCode":null,"errorMessage":"{key} must be an integer","messagePattern":"(.+?) must be an integer","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/auth_routes.py","lineNumber":724,"sourceCode":"        current = _load_settings()\n        # Per-key validation for numeric settings: coerce to int and clamp to a\n        # sane range so a bad value can't disable the agent or let it run away.\n        _INT_RANGES = {\n            \"agent_max_rounds\": (1, 200),\n            \"agent_max_tool_calls\": (0, 1000),  # 0 = unlimited\n        }\n        for key in DEFAULT_SETTINGS:\n            if key in RETIRED_SETTING_KEYS:\n                continue\n            if key not in body:\n                continue\n            val = body[key]\n            if key in _INT_RANGES:\n                lo, hi = _INT_RANGES[key]\n                try:\n                    val = int(val)\n                except (TypeError, ValueError):\n                    raise HTTPException(400, f\"{key} must be an integer\")\n                val = max(lo, min(val, hi))\n            current[key] = val\n        _save_settings(current)\n        return without_retired_settings(current)\n\n    # ---- Integrations CRUD ----\n\n    # Run migration on startup\n    migrate_from_settings()\n\n    @router.get(\"/integrations\")\n    async def list_integrations_route(request: Request):\n        \"\"\"List all integrations (admin only, keys masked).\"\"\"\n        user = _get_current_user(request)\n        if not user or not auth_manager.is_admin(user):\n            raise HTTPException(403, \"Admin only\")\n        items = load_integrations()\n        # Mask API keys for frontend display","sourceCodeStart":706,"sourceCodeEnd":742,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/auth_routes.py#L706-L742","documentation":"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).","triggerScenarios":"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.","commonSituations":"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\".","solutions":["Coerce to integer on the client before sending: Math.trunc(Number(value)) and skip the key when the result is NaN.","If fractional input is legitimate, send Math.round(value) or extend _INT_RANGES handling to accept floats.","Check the key name in the 400 message — it names the exact setting that failed validation.","Note the endpoint clamps to [lo, hi] after conversion, so only the type, not the range, matters here."],"exampleFix":"// before\nawait fetch('/api/settings', {method:'PUT', body: JSON.stringify({max_retries: retryInput.value})});\n\n// after\nconst n = Math.trunc(Number(retryInput.value));\nif (!Number.isFinite(n)) throw new Error('max_retries must be an integer');\nawait fetch('/api/settings', {method:'PUT', body: JSON.stringify({max_retries: n})});","handlingStrategy":"validation","validationCode":"const INT_SETTINGS = new Set(['max_retries', 'session_timeout', /* keys from _INT_RANGES */]);\nfunction sanitizeSettings(body) {\n  for (const [k, v] of Object.entries(body)) {\n    if (INT_SETTINGS.has(k)) {\n      const n = Math.trunc(Number(v));\n      if (!Number.isFinite(n)) throw new Error(`${k} must be an integer`);\n      body[k] = n;\n    }\n  }\n  return body;\n}","typeGuard":"const isIntLike = (v: unknown): v is number =>\n  (typeof v === 'number' && Number.isInteger(v)) ||\n  (typeof v === 'string' && /^-?\\d+$/.test(v.trim()));","tryCatchPattern":"try { await putSettings(body); } catch (e) {\n  if (e.status === 400 && /must be an integer/.test(e.message)) { /* flag the named field in the form */ }\n  else throw e;\n}","preventionTips":["Always run numeric form inputs through Number() + Number.isFinite() before PUT.","Keep a shared list of integer setting keys between frontend and backend.","Never send null for an integer setting; omit the key instead."],"tags":["validation","http-400","settings","type-coercion","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}