Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,29 @@ def test_status_mapping(self, status: int, expected_ok: bool) -> None:
assert ok is expected_ok, f"status={status}"
assert (error is None) is expected_ok, f"status={status}"

def test_request_exception_is_handled(self, monkeypatch: Any) -> None:
def test_request_exception_returns_retry_message_without_leaking_raw_error(self, monkeypatch: Any) -> None:
session = MagicMock()
session.get.side_effect = requests.ConnectionError("boom")
monkeypatch.setattr(vercel, "make_tracked_session", lambda *a, **k: session)

ok, error = validate_credentials("token")
assert ok is False
assert error == "boom"
assert error == vercel._VERCEL_UNREACHABLE_ERROR
assert "boom" not in (error or "")

@parameterized.expand([(429,), (500,), (503,)])
def test_transient_status_returns_retry_message_not_token_advice(self, status: int) -> None:
response = requests.Response()
response.status_code = status
session = MagicMock()
session.get.return_value = response
with patch.object(vercel, "make_tracked_session", lambda *a, **k: session):
ok, error = validate_credentials("token")

assert ok is False
assert error == vercel._VERCEL_UNREACHABLE_ERROR
# A transient Vercel-side error must not tell the user to fix their (possibly valid) token.
assert "Check that it's a valid token" not in (error or "")

def test_unexpected_status_does_not_leak_raw_status_code(self) -> None:
response = requests.Response()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
# a runaway guard, not a coverage limit — at 100 rows/page it allows ~1M rows before warning.
MAX_PAGES = 10_000

# Shown when the token check can't reach a verdict because Vercel is unreachable or returned a
# transient error (network failure, 429, 5xx). The token may be perfectly valid, so pointing the
# user at their credentials would send them chasing a problem they can't fix.
_VERCEL_UNREACHABLE_ERROR = "Couldn't reach Vercel to validate your access token. Please try again in a few minutes."


class VercelRetryableError(Exception):
pass
Expand Down Expand Up @@ -129,13 +134,19 @@ def validate_credentials(access_token: str) -> tuple[bool, str | None]:
response = make_tracked_session().get(
f"{VERCEL_BASE_URL}/v2/user", headers=_get_headers(access_token), timeout=10
)
except requests.exceptions.RequestException as e:
return False, str(e)
except requests.exceptions.RequestException:
# A network failure or timeout is transient and unrelated to the token; the raw exception
# embeds the URL and gives the user nothing actionable.
return False, _VERCEL_UNREACHABLE_ERROR

if response.status_code == 200:
return True, None
if response.status_code in (401, 403):
return False, "Invalid or unauthorized Vercel access token"
# 429 (rate limit) and 5xx are transient Vercel-side problems, not a bad token, so surface a
# retry hint rather than telling the user to fix credentials they can't fix.
if response.status_code == 429 or response.status_code >= 500:
return False, _VERCEL_UNREACHABLE_ERROR
return (
False,
"Couldn't validate your Vercel access token. Check that it's a valid token from your Vercel account settings, then try again.",
Expand Down
Loading