{"record":{"id":"3a271e93a071f71d","repo":"can1357/oh-my-pi","slug":"must-be-a-non-empty-string","errorCode":null,"errorMessage":"must be a non-empty string","messagePattern":"must be a non-empty string","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/robomp/src/config.py","lineNumber":435,"sourceCode":"        env_file_encoding=\"utf-8\",\n        extra=\"ignore\",\n        case_sensitive=False,\n    )\n\n    github_token: SecretStr = Field(..., alias=\"GITHUB_TOKEN\")\n    gh_proxy_hmac_key: SecretStr = Field(..., alias=\"ROBOMP_GH_PROXY_HMAC_KEY\")\n    gh_proxy_bind_host: str = Field(\"0.0.0.0\", alias=\"ROBOMP_GH_PROXY_BIND_HOST\")\n    gh_proxy_bind_port: int = Field(8081, alias=\"ROBOMP_GH_PROXY_BIND_PORT\")\n    workspace_root: Path = Field(Path(\"./data/workspaces\"), alias=\"ROBOMP_WORKSPACE_ROOT\")\n    log_dir: Path = Field(Path(\"./data/logs\"), alias=\"ROBOMP_LOG_DIR\")\n    gh_proxy_max_body_bytes: int = Field(1 << 20, alias=\"ROBOMP_GH_PROXY_MAX_BODY_BYTES\")\n    gh_proxy_git_timeout_seconds: float = Field(60.0, alias=\"ROBOMP_GH_PROXY_GIT_TIMEOUT_SECONDS\")\n\n    @field_validator(\"github_token\", \"gh_proxy_hmac_key\", mode=\"before\")\n    @classmethod\n    def _reject_blank(cls, value: object) -> object:\n        if isinstance(value, str) and not value.strip():\n            raise ValueError(\"must be a non-empty string\")\n        if hasattr(value, \"get_secret_value\"):\n            inner = value.get_secret_value()  # type: ignore[attr-defined]\n            if isinstance(inner, str) and not inner.strip():\n                raise ValueError(\"must be a non-empty string\")\n        return value\n\n\ndef load_proxy_settings() -> Settings:\n    \"\"\"Build a `Settings` instance suitable for the gh-proxy process.\n\n    Only the env vars the proxy actually consumes are required; the\n    orchestrator-only fields (webhook secret, bot_login, …) are set to\n    inert placeholders since `proxy.server` never reads them. Skips the\n    `Settings()` cross-field validator (which presumes orchestrator\n    semantics) by routing through `model_construct`.\n    \"\"\"\n    loader = _ProxyEnvLoader()  # type: ignore[call-arg]\n    return Settings.model_construct(","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/config.py#L417-L453","documentation":"Pydantic field validator `_reject_blank` rejects values for `github_token` and `gh_proxy_hmac_key` that are strings containing only whitespace. The library throws this because an empty or whitespace-only secret is never valid — it would silently produce unauthenticated or unsigned requests. It also unwraps pydantic SecretStr values (`get_secret_value`) before checking, so blank secrets wrapped in SecretStr are caught too.","triggerScenarios":"Constructing or loading Settings (e.g. via load_proxy_settings or Settings(...)) with `github_token=''`, `github_token='   '`, an env var like ROBOMP_GITHUB_TOKEN set to empty/whitespace, or a SecretStr('') passed in before validation.","commonSituations":"CI secrets not actually injected (empty env var), `.env` file with `ROBOMP_GITHUB_TOKEN=`, shell default expansion `${TOKEN:-}` resolving to empty, or copying config examples that leave the placeholder blank.","solutions":["Set the ROBOMP_GITHUB_TOKEN / ROBOMP_GH_PROXY_HMAC_KEY env var (or constructor arg) to a real non-empty value","Check the .env / secret source actually contains the value — an empty entry in a secrets manager or CI often serializes as ''","If the field is intentionally unused, omit it entirely rather than passing an empty string (leave it as None/unset if the schema allows)","Add a startup preflight that logs which specific env var was blank to speed diagnosis"],"exampleFix":"// before\nSettings(github_token=\"\", gh_proxy_hmac_key=SecretStr(\"  \"))\n// after\nSettings(github_token=\"ghp_xxxx\", gh_proxy_hmac_key=SecretStr(\"hmac-secret\"))","handlingStrategy":"validation","validationCode":"def ensure_secret(v):\n    if v is None: raise ValueError(\"github_token required\")\n    s = v.get_secret_value() if hasattr(v, \"get_secret_value\") else v\n    if not isinstance(s, str) or not s.strip(): raise ValueError(\"github_token must be a non-empty string\")\n    return v","typeGuard":"def has_secret(v) -> bool:\n    s = v.get_secret_value() if hasattr(v, \"get_secret_value\") else v\n    return isinstance(s, str) and bool(s.strip())","tryCatchPattern":"try:\n    settings = load_proxy_settings()\nexcept ValidationError as exc:\n    for e in exc.errors():\n        if \"non-empty string\" in str(e[\"msg\"]):\n            print(f\"blank secret at {e['loc']}: fix the env/config value\")\n    raise","preventionTips":["Fail fast at process start: validate Settings before doing any work","Never default secrets to \"\" — use None and an explicit is-set check","In CI, assert required secret vars are non-empty before the job's main steps","Use a linter/checker that flags `TOKEN=` empty entries in .env files"],"tags":["config","pydantic","validation","secrets"],"backgroundTag":"empty-required-config-value","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}