calesthio/OpenMontage · error · RuntimeError

Failed to load/refresh service-account credentials from {pat

Error message

Failed to load/refresh service-account credentials from {path}: {exc}

What it means

RuntimeError wrapping any exception raised while loading the JSON key (service_account.Credentials.from_service_account_file) or refreshing it (creds.refresh(Request())). The chained original exception (`from exc`) carries the true cause: malformed JSON, missing scopes on the key, clock skew, revoked key, or network failure reaching Google's token endpoint.

Source

Thrown at tools/google_credentials.py:129

        raise RuntimeError(
            "Service-account auth requires the 'google-auth' package. "
            "Install it with: pip install google-auth"
        ) from exc

    path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
    if not path or not os.path.exists(path):
        raise RuntimeError(
            "GOOGLE_APPLICATION_CREDENTIALS is not set or points to a missing "
            "file; cannot use service-account authentication."
        )

    try:
        creds = service_account.Credentials.from_service_account_file(
            path, scopes=scopes
        )
        creds.refresh(Request())
    except Exception as exc:  # noqa: BLE001 - re-raised as actionable message
        raise RuntimeError(
            f"Failed to load/refresh service-account credentials from {path}: {exc}"
        ) from exc

    token = creds.token
    if not token or not isinstance(token, str):
        raise RuntimeError(
            "Service-account credentials did not yield a valid access token."
        )

    project_id = getattr(creds, "project_id", None)
    ret_project_id = str(project_id) if project_id is not None else None
    return token, ret_project_id

View on GitHub (pinned to 95e1c3d0ab)

Solutions

  1. Read the tail of the chained exception text — it distinguishes JSON parse errors from network/401 failures.
  2. Validate the key file: it must be a service-account JSON with client_email and private_key fields: `python -c "import json;k=json.load(open(p));print(k['client_email'])"`.
  3. If refresh got 401/400 invalid_grant, re-download a fresh key from the console and update the env var; check the VM/service account is enabled.
  4. If it is a network error, verify outbound access to oauth2.googleapis.com:443 (proxy/cert settings).

Example fix

# before
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "key.yaml"  # wrong format -> RuntimeError('Failed to load/refresh ...')

# after
import json, os
key_path = "service-account.json"
json.load(open(key_path))["client_email"]  # fast structural validation
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = key_path
Defensive patterns

Strategy: try-catch

Validate before calling

import json, os
from pathlib import Path

def key_file_plausible() -> bool:
    p = Path(os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", ""))
    if not p.is_file():
        return False
    try:
        data = json.loads(p.read_text())
    except json.JSONDecodeError:
        return False
    return "client_email" in data and "private_key" in data

Try / catch

try:
    token, project = get_service_account_token()
except RuntimeError as e:
    cause = e.__cause__
    if cause and "invalid_grant" in str(cause):
        rotate_key_file()  # 401/invalid_grant -> refresh the key, then retry
    elif cause and isinstance(cause, OSError):
        check_network_egress("oauth2.googleapis.com")
    raise

Prevention

When it happens

Trigger: GOOGLE_APPLICATION_CREDENTIALS points to a file that is not a valid service-account JSON (human-readable key export, YAML, truncated download), the key's project/service account is disabled or deleted, or the refresh HTTP request fails (offline, proxy, DNS, SSL inspection).

Common situations: Pasting a Firebase/console key in the wrong format, expired or rotated keys still referenced, corporate proxies intercepting oauth2.googleapis.com, or system clock drift invalidating JWT assertions.

Related errors


AI-assisted analysis of calesthio/OpenMontage@95e1c3d0ab (2026-08-15). Data as JSON: /api/errors/dd8cdc548a21f1a3. Report an issue: GitHub.