Repository navigation
fix(web): an expired proxy session asks for a reload instead of reconnecting forever - #984
Conversation
…necting forever Behind an identity-aware proxy (Cloudflare Access in the reporter's setup), an expired proxy session made the API fetch receive a 302 to the proxy's login page on another origin. fetch cannot follow that, so the health probe saw a network error and the reconnect dialog never resolved; only a manual reload got out (spacering-net#981). API calls, health probes and uploads now send X-Requested-With: XMLHttpRequest, which Cloudflare Access answers with a 401 instead of the redirect (measured against a live Access application: 302 without the header, 401 with it). A 401 alone would then bounce the user to the token login and wipe a token that is still valid. Codeg's own 401 now carries WWW-Authenticate: Bearer realm="codeg", and the client reads that challenge to tell the two apart: Codeg's 401 keeps today's "Session expired" / "Go to login", any other 401 shows "Sign-in expired" with Reload (the proxy then runs its own login flow; the service worker passes navigations through, so a reload reaches it) and keeps the token. "Go to login" stays in that dialog as the way out if the proxy keeps refusing. The root page no longer clears the token on a proxy's 401, and the login page no longer calls the token invalid for one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@MagTer thanks, this one is ready from our side, with no changes. What I checked:
On the Reload-after-revoke gap you mentioned: the same Cloudflare page says that after a revoke, signing back in is blocked for up to a minute with an error, which matches what you saw. A natural expiry should take the normal login redirect instead, as the 302 you measured in #981 shows. If you get to try one on your setup, a note here is welcome, but it doesn't hold this PR. CI is green. The one red job in the first run was the |
Part 1 of #981 — the expired-session half. The opt-in proxy-JWT mode follows in a separate PR that builds on this one.
Problem
Behind an identity-aware proxy (Cloudflare Access here), once the proxy's session expires it answers the app's
fetch('/api/…')with a302to its login page on another origin.fetchcannot follow that,probeHealthsees a network error, and the reconnect dialog spins forever; only a manual reload gets out.Change
X-Requested-With: XMLHttpRequeston every API call —WebTransport.call, the health probe, the multipart and XHR uploads, the root page and the login page — through one helper,codegAuthHeaders()inweb-auth.ts. Cloudflare Access answers such a request with401instead of the redirect. Measured against a live Access application:POST /api/healthwithout a session returns302 → https://<team>.cloudflareaccess.com/cdn-cgi/access/login/…; the same request with the header returns401withWWW-Authenticate: Cloudflare-Access ….WWW-Authenticate: Bearer realm="codeg"(web/auth.rs). A 401 alone would otherwise send the user to the token login, whose "Go to login" wipes a token that is still valid. The client reads the challenge (unauthorizedSource()): Codeg's 401 keeps today's Session expired / Go to login; any other 401 shows a new Sign-in expired state with Reload (primary) and Go to login (secondary, the way out if the proxy keeps refusing), and keeps the token. A reload is a top-level navigation, which the service worker passes to the network (navigateNetworkFirst), so the proxy gets to run its login.WebConnection.proxySignInTitle/proxySignInDescriptionin all ten locales (reloadPageis reused). The non-English ones are my translations; corrections welcome.Tests
web-auth.test.ts(new),web-transport.test.ts,web-connection-guard.test.tsx,src/app/page.test.tsx(new),src/app/login/page.test.tsx(new). All 19 new cases were run against the unchanged code first and failed. One existing helper changed: the transport tests'resp401now carries Codeg's challenge, because that is what the server sends now.every_rejection_carries_the_codeg_challengeandan_accepted_request_carries_no_challengeinweb/auth.rs; the first was seen failing with the header removed.pnpm lint .,tsc --noEmit,pnpm test(all green; once,sidebar-tag-filter.test.tsx"keeps → for the search box in a right-to-left menu" failed under full-suite load and passed 3/3 in isolation, with and without this change),pnpm build,browser:agent:check/types;cargo test+clippy -D warningsfor the server and cli cells.web/auth.rs, which is not cfg-gated, so the server and cli cells compile the same code, but the desktop build itself is CI's to confirm.codeg-serverbehind a small fake proxy that behaves like Access (passes through until its session "expires", then 401 forX-Requested-WithAPI calls and a cross-origin 302 for everything else). Logged in with the token, expired the proxy session and dropped the sockets: the app showed Sign-in expired with Reload, the token stayed inlocalStorage, Reload went to the proxy's login, and after "signing in" again the workspace opened without the token prompt. The same script againstmain's frontend never reaches Sign-in expired.Against real Cloudflare Access (this branch plus part 2, built from source and run behind a live Access application): with the app open, I revoked my Access session in Zero Trust and the app showed Sign-in expired with Reload instead of the reconnect loop. Reload then landed on Cloudflare's own "revoked" page rather than the login, and getting back in took some fiddling on the Cloudflare side. As far as I can tell, that is how Access treats a revoked session: the app's cookie stays blocked, whereas a session that simply expires answers with a redirect to the login (the measurement above). Reload after a natural expiry has not been run against this build yet.
Closes part 1 of #981.
🤖 Generated with Claude Code