{"record":{"id":"aa0e1f7549c3cb83","repo":"headroomlabs-ai/headroom","slug":"settings-validation-failed-unknown-unknown-keys","errorCode":null,"errorMessage":"settings validation failed: unknown={unknown_keys} errors={field_errors}","messagePattern":"settings validation failed: unknown=(.+?) errors=(.+?)","errorType":"validation","errorClass":"SettingsValidationError","httpStatus":400,"severity":"error","filePath":"headroom/settings_store.py","lineNumber":848,"sourceCode":"    fails coercion. Returns the coerced dict (``None`` values dropped) on success.\n    \"\"\"\n    values = _normalize_values(values)\n    unknown = [key for key in values if key not in _BY_KEY]\n    field_errors: dict[str, str] = {}\n    coerced: dict[str, Any] = {}\n    for key, value in values.items():\n        field = _BY_KEY.get(key)\n        if field is None:\n            continue\n        try:\n            result = _coerce(field, value)\n        except (ValueError, TypeError) as exc:\n            field_errors[key] = str(exc)\n            continue\n        if result is not None:\n            coerced[key] = result\n    if unknown or field_errors:\n        raise SettingsValidationError(unknown, field_errors)\n    return coerced\n\n\ndef load() -> dict[str, Any]:\n    \"\"\"Return validated stored values. Fail-open: ``{}`` if missing or corrupt.\"\"\"\n    path = paths.settings_path()\n    try:\n        raw = path.read_text(encoding=\"utf-8\")\n    except FileNotFoundError:\n        return {}\n    except OSError as exc:\n        logger.warning(\"settings_store: cannot read %s: %s\", path, exc)\n        return {}\n    try:\n        data = json.loads(raw)\n    except (ValueError, UnicodeDecodeError) as exc:\n        logger.warning(\"settings_store: ignoring corrupt settings.json: %s\", exc)\n        return {}","sourceCodeStart":830,"sourceCodeEnd":866,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/settings_store.py#L830-L866","documentation":"SettingsValidationError is the aggregate error raised when a settings payload fails validation: unknown_keys lists keys not in the registry and field_errors maps each bad field key to its _coerce message (bool/number/range/enum/header-map failures). It carries structured detail (attributes unknown_keys, field_errors) precisely so the API layer can map unknown keys to HTTP 400 and field errors to 422. The f-string at line 729/848 renders both into the message.","triggerScenarios":"Calling the save/validate path with a payload containing an unrecognized key (typo like 'proxy_mod') or any invalid value that _coerce rejects (see errors 362-370). Also raised directly by _normalize_values when env aliases conflict (error 361).","commonSituations":"First-time integrations guessing setting names; stale clients from older versions sending renamed keys; bulk config imports where one bad row should not abort everything.","solutions":["Read the two lists in the message: fix or remove every key in unknown=, and correct each field in errors= using its per-field message.","If keys are unknown because of a version rename, update the client to the current key names in headroom.settings_store.SETTINGS.","Programmatically iterate exc.unknown_keys / exc.field_errors instead of parsing the message string."],"exampleFix":"# before\nsave({'proxy_mod': 'cache', 'max_retries': -1})  # SettingsValidationError\n\n# after\nsave({'proxy_mode': 'cache', 'max_retries': 0})","handlingStrategy":"try-catch","validationCode":"from headroom.settings_store import _BY_KEY, SETTINGS\n\ndef prevalidate(payload: dict) -> tuple[list, dict]:\n    unknown = [k for k in payload if k not in _BY_KEY]\n    errors = {}\n    for k, v in payload.items():\n        f = _BY_KEY.get(k)\n        if f is None:\n            continue\n        try:\n            _coerce(f, v)  # or replicate the checks per type\n        except ValueError as e:\n            errors[k] = str(e)\n    return unknown, errors","typeGuard":null,"tryCatchPattern":"from headroom.settings_store import SettingsValidationError\ntry:\n    store.save(payload)\nexcept SettingsValidationError as e:\n    bad = set(e.unknown_keys) | set(e.field_errors)\n    payload = {k: v for k, v in payload.items() if k not in bad}\n    store.save(payload)  # retry with clean subset; log what was dropped","preventionTips":["Map SettingsValidationError to 400 (unknown keys) / 422 (field errors) in HTTP layers, mirroring the library's intent.","Prevalidate payloads against the SETTINGS registry in tests.","Never parse the message string; use the structured attributes."],"tags":["settings","validation","api"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}