-
Notifications
You must be signed in to change notification settings - Fork 1.9k
fix(google-workspace): stop reporting connection_ok when nothing was checked #1707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,14 +91,27 @@ def _check_connection_impl(self, probe: 'Callable[[Any], None] | None' = None) - | |
| 'connection_ok': self._svc() is not None, | ||
| 'access': getattr(access, 'tier', None), | ||
| 'requiredScopes': list(getattr(access, 'scopes', []) or []), | ||
| # What was actually exercised, so a caller can tell a verified | ||
| # answer from an assumed one. Without this, `connection_ok: true` | ||
| # meant "we hold a plausible credential" while everyone read it as | ||
| # "this node works" — see #1694, where it sat next to a 403 saying | ||
| # the API was disabled and sent the reporter off re-authenticating. | ||
| 'checked': ['client', 'scopes'], | ||
| } | ||
| service = self._svc() | ||
| if probe is not None and service is not None: | ||
| try: | ||
| probe(service) | ||
| out['checked'].append('api') | ||
| except Exception as exc: | ||
| out['connection_ok'] = False | ||
| out['error'] = str(exc)[:200] | ||
| # Google distinguishes accessNotConfigured (API disabled) from | ||
| # forbidden (permission) from rateLimitExceeded, and that word | ||
| # alone names the fix. Losing it costs the reader the diagnosis. | ||
| reason = getattr(exc, 'reason', None) or getattr(exc, '_get_reason', lambda: None)() | ||
| if reason: | ||
| out['errorReason'] = str(reason).strip()[:120] | ||
| try: | ||
| cfg = Config.getNodeConfig(self.IGlobal.glb.logicalType, self.IGlobal.glb.connConfig) | ||
| auth_type = (cfg.get('authType') or 'service').strip() | ||
|
|
@@ -116,4 +129,15 @@ def _check_connection_impl(self, probe: 'Callable[[Any], None] | None' = None) - | |
| out['error'] = f'invalid user token data: {str(exc)[:160]}' | ||
| except Exception: | ||
| pass # config lookup diagnostics must never raise | ||
|
|
||
| # No live call was made, so `connection_ok` here only means the | ||
| # credential looks usable. Say so rather than letting it read as proof: | ||
| # a disabled API, an exhausted quota, disabled billing and an org-policy | ||
| # block are all invisible to a client-and-scopes check. | ||
| if 'api' not in out['checked'] and out['connection_ok']: | ||
| out['connection_ok'] = 'unknown' | ||
| out['note'] = ( | ||
| 'Credential and scopes look correct; no API call was made, so this ' | ||
| 'does not confirm the service is reachable or enabled.' | ||
| ) | ||
|
Comment on lines
+137
to
+142
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not claim scopes were verified after skipped diagnostics. The configuration block can fail silently, and token scope validation only runs for a non-empty user token. This branch can still emit “Credential and scopes look correct”; track whether those checks actually completed before producing that note. 🤖 Prompt for AI Agents |
||
| return out | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,7 +146,7 @@ def _id_list(args: dict, key: str, op: str) -> list[str]: | |
| ) | ||
| def check_connection(self, args: dict) -> dict: | ||
| """Check Gmail connection status and whether granted OAuth scopes cover the configured access tier. Read-only.""" | ||
| return self._check_connection_impl() | ||
| return self._check_connection_impl(probe=lambda s: execute(s.users().getProfile(userId='me'))) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Align both tool descriptions with the new diagnostic contract. The shared implementation no longer represents a scope-only boolean;
📍 Affects 2 files
🤖 Prompt for AI Agents🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: rg -n -A5 -B5 '_GMAIL_(SEND|SETTINGS).*SCOPE|def check_connection|getProfile' \
nodes/src/nodes/tool_google_workspace/gmail/IInstance.pyRepository: rocketride-org/rocketride-server Length of output: 3103 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the surrounding Gmail instance class and the shared connection helper.
wc -l nodes/src/nodes/tool_google_workspace/gmail/IInstance.py
sed -n '1,190p' nodes/src/nodes/tool_google_workspace/gmail/IInstance.py | cat -n
# Locate shared helper definitions.
rg -n "_check_connection_impl|def _check_connection_impl|class .*Access|scopes" nodes/src/nodes/tool_google_workspace/gmail -S
# Read any referenced service/auth helper files.
git ls-files nodes/src/nodes/tool_google_workspace/gmailRepository: rocketride-org/rocketride-server Length of output: 12773 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Locate shared access/connection definitions =="
fd -a 'core/google_access|.*access.*|IInstance\.py' nodes nodes/src 2>/dev/null | sed 's#^\./##' | head -50
rg -n "_check_connection_impl|_require_send|GMAIL|superset_scopes|allowed_scopes|GmailAccess|GMAIL_ACCESS|access tiers|readonly|send" nodes/core nodes/src/nodes/tool_google_workspace/gmail nodes -g '*access*' -g '*Instance.py' -S
echo "== Read relevant shared files =="
while IFS= read -r f; do
echo "--- $f ($(wc -l < "$f") lines) ---"
sed -n '1,260p' "$f" | cat -n
done < <(git ls-files | rg '(^nodes/.*/core/google_access|(^|/)IInstance\.py|(^|/)__init__\.py$|(^|/)client\.py$)' | grep -E 'gmail|google_access|tool_google_workspace' || true)Repository: rocketride-org/rocketride-server Length of output: 3588 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== google_access.py =="
wc -l nodes/src/nodes/core/google_access.py
sed -n '1,260p' nodes/src/nodes/core/google_access.py | cat -n
echo "== Gmail client.py scope helpers/usages =="
wc -l nodes/src/nodes/tool_google_workspace/gmail/client.py
sed -n '1,260p' nodes/src/nodes/tool_google_workspace/gmail/client.py | cat -n
echo "== Gmail node read-only diagnostics/messages scope checks =="
sed -n '190,380p' nodes/src/nodes/tool_google_workspace/gmail/IInstance.py | cat -nRepository: rocketride-org/rocketride-server Length of output: 31580 🌐 Web query:
💡 Result: To call the Gmail API method users.getProfile, you must use one of the following OAuth 2.0 scopes [1][2]: - https://mail.google.com/ - https://www.googleapis.com/auth/gmail.modify - https://www.googleapis.com/auth/gmail.compose - https://www.googleapis.com/auth/gmail.readonly - https://www.googleapis.com/auth/gmail.metadata You should choose the most narrowly focused scope that provides the access your application requires [3]. For example, if your application only needs to read email metadata (which is what users.getProfile primarily provides), you may find that the gmail.metadata scope is the most appropriate, least-privileged choice [4]. Citations:
Use a read-only probe for
🤖 Prompt for AI AgentsSource: MCP tools |
||
|
|
||
| # ======================================================================= | ||
| # MESSAGES — read | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
rg -n -A35 -B3 'def execute|errorReason|_get_reason|\.reason' \ nodes/src/nodes/tool_google_workspace/google_client.py \ nodes/src/nodes/tool_google_workspace/IInstance.pyRepository: rocketride-org/rocketride-server
Length of output: 11690
🏁 Script executed:
Repository: rocketride-org/rocketride-server
Length of output: 33096
🏁 Script executed:
Repository: rocketride-org/rocketride-server
Length of output: 2493
🌐 Web query:
Google API Python client HttpError reason attribute source _get_reason💡 Result:
In the Google API Python client (
google-api-python-client), the_get_reasonmethod is a private helper function defined within theHttpErrorclass ingoogleapiclient/errors.py[1][2]. Purpose and Functionality: The_get_reasonmethod is responsible for calculating and extracting the descriptive reason for an HTTP error from the response content [1][3][2]. Because Google APIs often return error information in a JSON-formatted body rather than just the standard HTTP status reason,_get_reasonperforms the following steps: 1. It attempts to parse theself.contentattribute (the raw response body) as JSON [1][2]. 2. If successful, it navigates the JSON structure—typically looking under theerrorkey—to extract themessagefield [1][2]. 3. It also attempts to identify and populate theself.error_detailsattribute by checking for common error detail keys such asdetail,details,errors, ormessage[1][2]. 4. If the content is not valid JSON or the expected keys are missing, it falls back to the defaultself.resp.reasonprovided by the underlying HTTP response [1][2]. Usage Context: You should not typically call_get_reasondirectly in your application code, as it is intended for internal use by theHttpErrorclass to populate its attributes [1][2]. Historically, developers found that fields likeerror_detailswere not always reliably populated unless_get_reasonor thereprmethod had been triggered [4]. In modern versions of the library, theHttpErrorclass is designed to handle this extraction internally, often during initialization [1][4]. If you need to access specific error information, such as the machine-readablereasonfield often found in Google API error responses (e.g., 'accessNotConfigured'), the library does not expose a dedicatedreasonattribute [5]. Instead, the recommended approach is to parse thecontentattribute of theHttpErrorobject yourself using thejsonmodule [5]: import json try: #... call your API method... except HttpError as e: error_data = json.loads(e.content) # Access the specific reason field from the API error structure api_reason = error_data.get('error', {}).get('errors', [{}]).get('reason') print(f"API Error Reason: {api_reason}")Citations:
Preserve Google error details before wrapping
executefailures.google_client.executeconvertsHttpErrorinto aValueErrorbefore_check_connection_implsees it, sogetattr(exc, 'reason', None)/_get_reason()cannot populateerrorReasonfor live Gmail/Drive probe failures. Preserve the parsed Google error reason fromexc.contentbefore raising, or attach it to the replacement exception.🤖 Prompt for AI Agents