hermes - 💡(How to fix) Fix google-workspace: use structured RefreshError.args[1]['error'] instead of str(e) substring matching in check_auth() [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Follow-up to #19570 / PR #19643 (salvaged and merged as 5fa493a2c + 83c23e886). The shipped version of check_auth() in skills/productivity/google-workspace/scripts/setup.py classifies OAuth refresh errors by substring-matching against str(e).lower():

except Exception as e:
    err_str = str(e).lower()
    if "disabled_client" in err_str or "invalid_client" in err_str:
        print(f"OAUTH_CLIENT_DISABLED: {e}")
        ...
    elif "token_revoked" in err_str or "invalid_grant" in err_str:
        print(f"TOKEN_REVOKED: {e}")
        ...
    else:
        print(f"REFRESH_FAILED: {e}")

This works today because google-auth's default RefreshError repr happens to contain the OAuth 2.0 error code. But the library already parses that code for us — we're throwing away structured data and re-deriving it from a string.

Error Message

except Exception as e: err_str = str(e).lower() if "disabled_client" in err_str or "invalid_client" in err_str: print(f"OAUTH_CLIENT_DISABLED: {e}") ... elif "token_revoked" in err_str or "invalid_grant" in err_str: print(f"TOKEN_REVOKED: {e}") ... else: print(f"REFRESH_FAILED: {e}")

Root Cause

This works today because google-auth's default RefreshError repr happens to contain the OAuth 2.0 error code. But the library already parses that code for us — we're throwing away structured data and re-deriving it from a string.

Fix Action

Fixed

Code Example

except Exception as e:
    err_str = str(e).lower()
    if "disabled_client" in err_str or "invalid_client" in err_str:
        print(f"OAUTH_CLIENT_DISABLED: {e}")
        ...
    elif "token_revoked" in err_str or "invalid_grant" in err_str:
        print(f"TOKEN_REVOKED: {e}")
        ...
    else:
        print(f"REFRESH_FAILED: {e}")

---

from google.auth.exceptions import RefreshError

def _extract_oauth_error_code(e: Exception) -> str:
    """Pull the structured OAuth error code from a google-auth RefreshError.

    Falls back to substring matching on str(e) only when the structured
    response_data dict is absent (very old google-auth, or non-RefreshError).
    """
    if isinstance(e, RefreshError) and len(e.args) >= 2 and isinstance(e.args[1], dict):
        return str(e.args[1].get("error") or "").lower()
    # Fallback for unusual exception shapes
    s = str(e).lower()
    for code in ("disabled_client", "invalid_client", "token_revoked", "invalid_grant"):
        if code in s:
            return code
    return ""

---

except Exception as e:
    code = _extract_oauth_error_code(e)
    if code in ("disabled_client", "invalid_client"):
        ...
    elif code in ("token_revoked", "invalid_grant"):
        ...
    else:
        print(f"REFRESH_FAILED: {e}")
RAW_BUFFERClick to expand / collapse

Context

Follow-up to #19570 / PR #19643 (salvaged and merged as 5fa493a2c + 83c23e886). The shipped version of check_auth() in skills/productivity/google-workspace/scripts/setup.py classifies OAuth refresh errors by substring-matching against str(e).lower():

except Exception as e:
    err_str = str(e).lower()
    if "disabled_client" in err_str or "invalid_client" in err_str:
        print(f"OAUTH_CLIENT_DISABLED: {e}")
        ...
    elif "token_revoked" in err_str or "invalid_grant" in err_str:
        print(f"TOKEN_REVOKED: {e}")
        ...
    else:
        print(f"REFRESH_FAILED: {e}")

This works today because google-auth's default RefreshError repr happens to contain the OAuth 2.0 error code. But the library already parses that code for us — we're throwing away structured data and re-deriving it from a string.

Problem

  1. Fragile against description changes. str(e) format is "<error_code>: <error_description>". Google localizes and rewrites error_description over time; nothing stops a future description like "Access to this invalid_client was not granted" from tripping the invalid_client branch when the real error is something else.

  2. False positives on compound codes. Substring "invalid_grant" also matches "invalid_grant_type" — a distinct error some providers use.

  3. Throws away the library's parse work. google.auth.exceptions.RefreshError is raised as RefreshError(error_details, response_data, retryable=...) (source). e.args[1] is the token-endpoint response dict with a stable "error" field whose value is the exact RFC 6749 error code. We should read that directly.

Proposed fix

from google.auth.exceptions import RefreshError

def _extract_oauth_error_code(e: Exception) -> str:
    """Pull the structured OAuth error code from a google-auth RefreshError.

    Falls back to substring matching on str(e) only when the structured
    response_data dict is absent (very old google-auth, or non-RefreshError).
    """
    if isinstance(e, RefreshError) and len(e.args) >= 2 and isinstance(e.args[1], dict):
        return str(e.args[1].get("error") or "").lower()
    # Fallback for unusual exception shapes
    s = str(e).lower()
    for code in ("disabled_client", "invalid_client", "token_revoked", "invalid_grant"):
        if code in s:
            return code
    return ""

Then in check_auth():

except Exception as e:
    code = _extract_oauth_error_code(e)
    if code in ("disabled_client", "invalid_client"):
        ...
    elif code in ("token_revoked", "invalid_grant"):
        ...
    else:
        print(f"REFRESH_FAILED: {e}")

Acceptance criteria

  • check_auth() uses _extract_oauth_error_code(e) for classification, not raw substring matching.
  • Fallback path preserved for non-RefreshError exceptions (network, etc.).
  • Tests added in tests/skills/test_google_oauth_setup.py that inject a fake Credentials whose .refresh() raises RefreshError("disabled_client: The OAuth client was disabled.", {"error": "disabled_client", "error_description": "..."}) and assert the OAUTH_CLIENT_DISABLED: marker is printed. Mirror for invalid_grant.

References

Related

See also #21861 (partial-scope check_auth_live false positive) and #21862 (no test coverage for either branch) — worth doing in one PR.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix google-workspace: use structured RefreshError.args[1]['error'] instead of str(e) substring matching in check_auth() [1 pull requests]