{"record":{"id":"b79601c2f91cdb3a","repo":"PrefectHQ/fastmcp","slug":"the-horizon-api-key-is-invalid","errorCode":null,"errorMessage":"The Horizon API key is invalid","messagePattern":"The Horizon API key is invalid","errorType":"exception","errorClass":"StateFileError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/cli/deploy/credentials.py","lineNumber":75,"sourceCode":"class CredentialStore:\n    \"\"\"Persist the active personal Horizon API key.\"\"\"\n\n    def __init__(self, state_directory: Path | None = None) -> None:\n        if state_directory is None:\n            import fastmcp\n\n            state_directory = fastmcp.settings.home / \"cli\"\n        self.path = state_directory / \"auth.json\"\n\n    def load(self) -> SecretStr | None:\n        state = read_state(self.path, AuthState, secret=True)\n        return state.api_key if state is not None else None\n\n    def save(self, api_key: SecretStr | str) -> None:\n        try:\n            state = AuthState(schemaVersion=1, apiKey=api_key)\n        except ValidationError:\n            raise StateFileError(\"The Horizon API key is invalid\") from None\n        write_state(\n            self.path,\n            {\n                \"schemaVersion\": state.schema_version,\n                \"apiKey\": state.api_key.get_secret_value(),\n            },\n        )\n\n    def save_for_origin(\n        self,\n        api_key: SecretStr | str,\n        *,\n        expected_api_origin: str,\n    ) -> None:\n        \"\"\"Save a key only while its issuing Horizon origin is active.\"\"\"\n        from fastmcp.cli.deploy.configuration import ConfigurationStore\n\n        expected_api_origin = normalize_api_origin(expected_api_origin)","sourceCodeStart":57,"sourceCodeEnd":93,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/cli/deploy/credentials.py#L57-L93","documentation":"CredentialStore.save validates the key by constructing AuthState; any pydantic ValidationError (e.g. empty key) is caught and converted to StateFileError('The Horizon API key is invalid') so callers get a single CLI-friendly error type instead of a pydantic traceback.","triggerScenarios":"Calling save (directly, via save_for_origin, or via resolve_credential) with a SecretStr or str that fails AuthState validation — most commonly empty/whitespace values.","commonSituations":"Login scripts reading an unset env var; pasted keys with stray whitespace; empty secrets in CI secret managers surfaced as ''.","solutions":["Pass a non-empty Horizon API key string/SecretStr to save()","Strip whitespace from the key before saving","Re-run `fastmcp deploy login` to obtain and store a valid key","Inspect the upstream secret source for empty values"],"exampleFix":"// before\nstore.save(config.get(\"apiKey\"))  # None/empty -> StateFileError\n// after\nkey = (config.get(\"apiKey\") or \"\").strip()\nif key:\n    store.save(key)","handlingStrategy":"validation","validationCode":"key = os.environ.get(\"HORIZON_API_KEY\", \"\").strip()\nif not key:\n    raise SystemExit(\"HORIZON_API_KEY is not set\")","typeGuard":"def is_valid_api_key(key: str | SecretStr) -> bool:\n    raw = key.get_secret_value() if isinstance(key, SecretStr) else key\n    return bool(raw and raw.strip())","tryCatchPattern":"from fastmcp.cli.deploy.credentials import StateFileError\ntry:\n    store.save(api_key)\nexcept StateFileError as e:\n    print(f\"Credential not saved: {e}; re-run `fastmcp deploy login`\")","preventionTips":["Validate the key is non-empty before calling save()","Strip whitespace from user-pasted keys","Source keys from a verified secret store, not possibly-empty env vars"],"tags":["pydantic","validation","api-key","state-file"],"backgroundTag":"invalid-api-key","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}