fix(skills): add timeout to Google OAuth urlopen calls

This commit is contained in:
Zyrixtrex 2026-05-19 00:11:38 -07:00 committed by Teknium
parent b8a9cbd18c
commit 87c6edc1d0
4 changed files with 53 additions and 3 deletions

View file

@ -586,7 +586,8 @@ def revoke(email: Optional[str] = None) -> None:
f"https://oauth2.googleapis.com/revoke?token={creds.token}",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
),
timeout=15,
)
print("Token revoked with Google.")
except Exception as exc:

View file

@ -51,13 +51,16 @@ def refresh_token(token_data: dict) -> dict:
req = urllib.request.Request(token_data["token_uri"], data=params)
try:
with urllib.request.urlopen(req) as resp:
with urllib.request.urlopen(req, timeout=15) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")
print(f"ERROR: Token refresh failed (HTTP {e.code}): {body}", file=sys.stderr)
print("Re-run setup.py to re-authenticate.", file=sys.stderr)
sys.exit(1)
except (urllib.error.URLError, TimeoutError) as e:
print(f"ERROR: Token refresh failed (network): {e}", file=sys.stderr)
sys.exit(1)
token_data["token"] = result["access_token"]
token_data["expiry"] = datetime.fromtimestamp(

View file

@ -411,7 +411,8 @@ def revoke():
f"https://oauth2.googleapis.com/revoke?token={creds.token}",
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
),
timeout=15,
)
print("Token revoked with Google.")
except Exception as e:

View file

@ -103,6 +103,51 @@ def test_bridge_refreshes_expired_token(bridge_module, tmp_path):
assert saved["type"] == "authorized_user"
def test_bridge_refresh_passes_timeout_to_urlopen(bridge_module):
"""Token refresh must pass an explicit timeout so a hung Google endpoint
cannot block the agent turn indefinitely (no `timeout=` defaults to the
global socket timeout, which is unset)."""
past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.old", expiry=past)
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps({
"access_token": "ya29.refreshed",
"expires_in": 3600,
}).encode()
mock_resp.__enter__ = lambda s: s
mock_resp.__exit__ = MagicMock(return_value=False)
with patch("urllib.request.urlopen", return_value=mock_resp) as mocked:
bridge_module.get_valid_token()
assert mocked.call_count == 1
_, kwargs = mocked.call_args
assert kwargs.get("timeout") is not None, (
"urlopen call must pass timeout= to avoid hanging on unreachable upstream"
)
def test_bridge_refresh_exits_cleanly_on_network_error(bridge_module):
"""URLError/timeout during refresh exits 1 with a readable message
instead of crashing with a raw traceback."""
import urllib.error
past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
token_path = bridge_module.get_token_path()
_write_token(token_path, token="ya29.old", expiry=past)
with patch(
"urllib.request.urlopen",
side_effect=urllib.error.URLError("timed out"),
):
with pytest.raises(SystemExit) as exc_info:
bridge_module.get_valid_token()
assert exc_info.value.code == 1
def test_bridge_exits_on_missing_token(bridge_module):
"""Missing token file causes exit with code 1."""
with pytest.raises(SystemExit):