From 9590219977bcb873c343e1411543a208e3eac21a Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Wed, 4 Nov 2026 20:37:54 +0700 Subject: [PATCH] fix(stockbit): end a browser sign-in on a CDN block A block (UPSTREAMBLOCKED) was treated as a passing failure, so idx's browser re-checked the same cookie every 5 s until the 15-minute timeout, warning "does not retry a block ... retrying". A block holds for every session from this network: the wait now ends with it at once. Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY --- crates/idx-core/src/api/stockbit/browser.rs | 35 +++++++++++++++++++++ crates/idx-core/src/api/stockbit/mod.rs | 11 ++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/crates/idx-core/src/api/stockbit/browser.rs b/crates/idx-core/src/api/stockbit/browser.rs index 3e94ce5..edf4f36 100644 --- a/crates/idx-core/src/api/stockbit/browser.rs +++ b/crates/idx-core/src/api/stockbit/browser.rs @@ -271,6 +271,12 @@ fn wait_for_session( )); retry_at = Some(Instant::now() + VERIFY_RETRY); } + // A block holds for every session from this network, so + // the wait ends with it rather than at the timeout. + Err(err @ IdxError::UpstreamBlocked(_)) => { + devtools.close(); + return Err(err); + } Err(err) => { crate::runtime::warn(format!( "Stockbit refused the session in idx's browser ({err}); sign in \ @@ -1041,6 +1047,35 @@ mod tests { let _ = std::fs::remove_dir_all(&profile); } + /// A CDN block is never retried: the wait ends with it at once, not at + /// the timeout. + #[test] + fn a_blocked_check_ends_the_wait() { + let profile = temp_dir("blocked"); + let browser = FakeBrowser::start(Fake::default()); + write_endpoint(&profile, browser.port); + let mut devtools = connect(&profile, &|| true).unwrap(); + let checks = std::cell::Cell::new(0); + let started = Instant::now(); + let err = wait_for_session( + &mut devtools, + Duration::from_secs(30), + &|_| { + checks.set(checks.get() + 1); + Err(IdxError::UpstreamBlocked( + "HTTP 403 from its CDN".to_string(), + )) + }, + &|| true, + ) + .unwrap_err(); + assert!(matches!(err, IdxError::UpstreamBlocked(_)), "{err}"); + assert_eq!(checks.get(), 1); + assert!(started.elapsed() < Duration::from_secs(5)); + browser.finish(); + let _ = std::fs::remove_dir_all(&profile); + } + /// Closing the window ends the wait at once, not at the timeout. #[test] fn closing_the_window_ends_the_wait() { diff --git a/crates/idx-core/src/api/stockbit/mod.rs b/crates/idx-core/src/api/stockbit/mod.rs index d8bca3a..eba50a5 100644 --- a/crates/idx-core/src/api/stockbit/mod.rs +++ b/crates/idx-core/src/api/stockbit/mod.rs @@ -295,12 +295,10 @@ fn keep_after_failed_check(err: &IdxError, rotated: bool) -> bool { } /// Whether a session check failed for a reason that says nothing about the -/// session itself, so the same session may be checked again later. +/// session itself, so the same session may be checked again later. A CDN +/// block (`UpstreamBlocked`) is not: idx never retries a block. pub(crate) fn check_is_transient(err: &IdxError) -> bool { - matches!( - err, - IdxError::Http(_) | IdxError::RateLimited | IdxError::UpstreamBlocked(_) - ) + matches!(err, IdxError::Http(_) | IdxError::RateLimited) } fn restore(store: &SessionStore, previous: Option>) { @@ -343,6 +341,9 @@ mod tests { assert!(check_is_transient(&IdxError::RateLimited)); assert!(check_is_transient(&network)); assert!(!check_is_transient(&refused)); + assert!(!check_is_transient(&IdxError::UpstreamBlocked( + "403".to_string() + ))); assert!(!check_is_transient(&IdxError::InvalidInput( "x".to_string() )));