Skip to content

Commit 3b07d40

Browse files
committed
fix: revert CAS lck early-exit, bump outer login retries to 5
The /login early-exit added in e3c2bde caused worse failures than the intermittent CAS hiccups it was meant to skip past — when the CAS chain legitimately hops through a /login-shaped URL mid-redirect (which it sometimes does even on a healthy session), the inner loop aborted before reaching the eventual lck-bearing hop. Removed it; the chain is now chased to completion as in dae2514. Two small optimizations on top: - bump login_with_retry from 3 to 5 outer attempts so the long tail of back-to-back transient CAS rejections (which the user has historically ridden out with ~5 retries) doesn't crash the whole run. - when lck extraction fails, include the last 3 redirect Locations in the RuntimeError so future failures are debuggable without rerunning with extra prints. Also include the exception message in the outer retry log line.
1 parent e47cd5d commit 3b07d40

2 files changed

Lines changed: 27 additions & 16 deletions

File tree

main.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,16 @@
3131
from src.api.webvpn import WebVPNSession
3232

3333

34-
def login_with_retry(max_attempts: int = 3) -> WebVPNSession:
35-
"""Login to WebVPN + iCourse CAS, retrying on transient failures."""
34+
def login_with_retry(max_attempts: int = 5) -> WebVPNSession:
35+
"""Login to WebVPN + iCourse CAS, retrying on transient failures.
36+
37+
The iCourse CAS step (authenticate_icourse) has its own inner retry
38+
loop for transient redirect-chain hiccups; this outer loop only runs
39+
when those inner retries are exhausted, which generally means the
40+
WebVPN session itself needs a fresh login. 5 attempts handles the
41+
long tail of times when CAS rejects multiple fresh sessions in a
42+
row before letting one through.
43+
"""
3644
for attempt in range(max_attempts):
3745
try:
3846
vpn = WebVPNSession()
@@ -43,8 +51,8 @@ def login_with_retry(max_attempts: int = 3) -> WebVPNSession:
4351
return vpn
4452
except Exception as e:
4553
if attempt < max_attempts - 1:
46-
print(f" Failed: {type(e).__name__}, retrying...")
47-
time.sleep(3)
54+
print(f" Failed: {type(e).__name__}: {e}; retrying...")
55+
time.sleep(5)
4856
else:
4957
raise
5058

src/api/webvpn.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -199,27 +199,26 @@ def authenticate_icourse(
199199
vpn_url = get_vpn_url(casapi_url)
200200

201201
# Follow redirect chain to reach IDP login page and extract lck.
202-
# The CAS gateway intermittently returns a 200 interstitial or
203-
# bounces back to WebVPN's own /login (cookie not yet effective
204-
# for the casapi vhost) right after a successful WebVPN login.
205-
# Retry a few times in-place before letting login_with_retry()
206-
# waste a full WebVPN re-login on a transient hiccup.
202+
# The CAS gateway intermittently returns a 200 interstitial or a
203+
# 302 to a `/login`-shaped page right after a successful WebVPN
204+
# login. Both are transient — chase the full redirect chain (lck
205+
# often appears mid-chain even when an intermediate hop looks
206+
# like a stale-session bounce) and retry a few times before
207+
# letting login_with_retry() waste a full WebVPN re-login.
207208
import time as _time
208209
lck = None
210+
last_locations: list[str] = []
209211
for cas_attempt in range(3):
210212
if cas_attempt > 0:
213+
_time.sleep(2)
211214
print(f" CAS lck extract retry {cas_attempt}/2...")
212-
_time.sleep(3)
213215
resp = self.session.get(vpn_url, allow_redirects=False, timeout=60)
216+
last_locations = []
214217
for _ in range(15):
215218
location = resp.headers.get("Location", "")
216219
if resp.status_code not in (301, 302, 303, 307) or not location:
217220
break
218-
# WebVPN bouncing to its own /login means the session
219-
# isn't recognised for this vhost — stop chasing the
220-
# heavy login page and let the outer retry try again.
221-
if "/login" in location:
222-
break
221+
last_locations.append(location)
223222
lck_match = re.search(r'lck=([^&#"]+)', location)
224223
if lck_match:
225224
lck = lck_match.group(1)
@@ -241,8 +240,12 @@ def authenticate_icourse(
241240
if lck:
242241
break
243242
if not lck:
243+
# Log the redirect trail so future failures are diagnosable
244+
# without re-running with extra prints.
245+
trail = " -> ".join(last_locations[-3:]) if last_locations else "<no redirects>"
244246
raise RuntimeError(
245-
f"Failed to extract lck from CAS redirect chain (status={resp.status_code})"
247+
f"Failed to extract lck from CAS redirect chain "
248+
f"(status={resp.status_code}, last hops: {trail})"
246249
)
247250

248251
entity_id = config.ICOURSE_BASE

0 commit comments

Comments
 (0)