08.08 — Chrome BFCache (B-BROWSER-1) + environment artifacts#
Chrome restores a
no-storepage after logout, from the BFCache, even though it’s a page protected by authentication.
B-BROWSER-1 — Chrome’s BFCache is a pain#
Symptom#
After logout, clicking the back button to be redirected to a page that was protected by authentication (e.g. history.php):
- Chrome: the page stays displayed with its contents as shown to a still-authenticated user. The URL remains
history.php, and the server isn’t re-contacted to validate or invalidate access. - Firefox: a new request goes to the server, which sees the session is dead and redirects.
Very annoying gap#
history.php correctly sends:
Cache-Control: no-store, no-cache, must-revalidatePragma: no-cacheExpires
And server-side session invalidation works correctly.
But Chrome’s BFCache restores the page anyway from a local snapshot.
Confirmation recipe: back-then-reload#
back() → we're on history.php, authenticated page restored
↓ (nothing touched the server)
refresh() → reload forces a server round-trip
↓
server: session dead → 302 to homepage
↓
we are redirectedback()shows us the page again we shouldn’t see anymore ANDrefresh()causes a redirect → confirmed BFCache hit.back()redirects immediately → server-side invalidation working as expected.back()shows us the page again we shouldn’t see anymore ANDrefresh()doesn’t redirect → server-side invalidation is broken (anomaly).
CLAUDE.md:
A confirmed BFCache exposure raises a dedicated, non-transient exception —
BackForwardCacheExposureError(src/lib/errors.py). Never a bareAssertionError; never a SeleniumWebDriverException. The finding is deterministic and must never land intransient_errors.
Dedicated exception#
# src/lib/errors.py
class BackForwardCacheExposureError(Exception):
"""The browser's back-forward cache exposed a logged-out authenticated view."""Not in transient_errors. This finding is deterministic — we observe it immediately and it’s never a flake.
Related tests#
| Test | Recipe |
|---|---|
post_logout_bfcache_exposure.py | back-then-reload: if back lingers on a page that shouldn’t be shown + refresh redirects → BFCacheExposureError |
post_logout_server_invalidation.py | direct refresh: confirms the server does invalidate (PASS) |
post_logout_frenetic_navigation.py | back/forward stress (3 cycles): tries to provoke a mysterious BFCache timing |
post_logout_bfcache_exposure: FAIL on Chrome, PASS on Firefox.post_logout_server_invalidation: PASS on both (the server does its part).
Code#
class HistoryPage(SeleniumBackAndForwardNavigationMixin, SeleniumTitleMixin, POMBase):
def verify_back_button_did_not_restore_view(self) -> Self:
# We're on history.php after back()
pre_url = self._driver.current_url
self._driver.refresh()
try:
WebDriverWait(self._driver, get_timeout()).until(
ec.url_changes(pre_url)
)
# ✅ refresh redirected → server-side invalidation works
# → back() stayed put = BFCache hit
raise BackForwardCacheExposureError(
f"Browser BFCache restored authenticated view of {pre_url} after logout."
)
except TimeoutException:
# refresh didn't redirect → BFCache OR server-side fail
# ... distinguish ...
raiseThe back→refresh distinction is encapsulated in the POM. The scenario just calls verify_back_button_did_not_restore_view.
“Cross-browser difference is a finding”#
CLAUDE.md:
Never skip a test on the browser where it fails.
This test stays active on both browsers.
- On Chrome, it deterministically documents the fragility.
- On Firefox, it documents that Firefox honors
no-store.
Recommendation to CURA#
IDENTIFIED_GAPS.md:
Recommendation for CURA: send
Clear-Site-Data: "cache", "cookies"on the logout response so the snapshot is evicted regardless of the browser’s back-forward-cache policy.
This header forces the browser to destroy the cached snapshot regardless of BFCache policy.
A-ENV-1 — Rapid POST drop under --workers 3#
Symptom#
Under --workers 3, a rapid consecutive POST from one of the three parallel workers fails intermittently.
Observed on:
Appointments - Saturation(5 successive bookings).Journey - History ordered ...(2 successive bookings).
Cause#
Heroku eco-dyno concurrency limit hit by near-simultaneous POSTs. Infra issue, not a SUT bug. In single-worker mode, 2 consecutive bookings go through in 200–600ms.
Flakiness handling#
TimeoutException ⊆ WebDriverException → in transient_errors → auto-retry. A failure surviving all retries means contention was too high — not a regression, but worth a DevOps follow-up.
A-ENV-2 — Weak-password detection modal (Chrome)#
Symptom#
On Chrome only, after a successful login, every subsequent action (click, keystroke, enter) did nothing. Root cause: a native Chrome modal — “your password has leaked” — appeared and swallowed all input.
Cause#
ThisIsNotAPassword is in Google’s publicly-leaked credentials database. Chrome’s password manager fires right after any successful login with this credential — a test-environment issue that hits the tester directly.
Resolution#
src/lib/ext/ocarina/adapters/selenium/create_drivers_pool.py:
options.add_experimental_option("prefs", {
"credentials_enable_service": False,
"profile.password_manager_enabled": False,
"profile.password_manager_leak_detection": False,
})
options.add_argument("--disable-features=PasswordLeakDetection")Disables Chrome’s password manager and leak detection. The modal disappears.
IDENTIFIED_GAPS.md#
Why it stays documented: the entry preserves why the chrome driver adapter exists — delete the adapter and this artifact returns.
Institutional memory. If a future refactor removes the adapter, we’ll know why it existed.
Semantic conventions#
| Prefix | Category | Example |
|---|---|---|
G- | Gap (CURA is broken) | G-SEC-1, G-DATA-1, G-SPEC-1 |
B- | Browser (browser behavior) | B-BROWSER-1 |
A- | Environment artifact | A-ENV-1 (dyno contention), A-ENV-2 (Chrome modal) |