Repository navigation
Conversation
There was a problem hiding this comment.
Changes requested. This recovers when an SSO proxy answers 401, but it also catches the 401s that OpenCode's own server sends, which the app uses for its sign-in screen. I couldn't test it end to end because I have no SSO proxy or real browser. The PR's test and packages/app typecheck pass. I also ran a quick probe of my own: a password-protected opencode serve that keeps returning 401 on /api/info. With the app's 10s health polling, the wrapper ran location.replace('/?auth_refresh=…') every 60s.
Public API: unchanged.
Concerns:
- Your own server's 401s are treated as an expired proxy session (inline). With a server password, the app relies on a same-origin 401 to show its sign-in screen. Now the page reloads every 60s and wipes what the user is typing there.
- Overlap with #52223. It tackles the same expired-proxy-session problem (#50481, #57649) and also sets
platform.fetchinweb.ts, so the two PRs conflict. It confirms the proxy is redirecting (probes/api/infowithredirect: "manual") before reloading, and reloads with a?reauth=1the service worker lets through, rather than removing the service worker. Adding 401 handling to that approach, with the same check that the 401 comes from the proxy rather than OpenCode, is probably simpler than keeping two separate wrappers. Coordinating there would help. - The service worker is removed on every recovery (inline). Offline/PWA support is lost until the next registration.
- The reload goes to a hardcoded
/(inline). It drops the current route and ignores a base path when the app is served under a prefix. - You note the SSO flow itself hasn't been tested end to end yet. A run behind Authentik or Authelia would show the 401 path actually ends up on the sign-in page.
| async (input: RequestInfo | URL, init?: RequestInit) => { | ||
| const response = await fetch(input, init) | ||
| const url = new URL(input instanceof Request ? input.url : input, location.href) | ||
| if (response.status !== 401 || url.origin !== location.origin || auth.refreshing) return response |
There was a problem hiding this comment.
Any same-origin 401 sets this off, including OpenCode's own UnauthorizedError when the server has a password. The web app counts on that 401 (health.unauthorized) to show its sign-in screen. The health check repeats every 10s, so the 60s limit on line 11 becomes a reload once a minute instead of a loop. Each reload clears the sign-in screen and sends the user to /. I reproduced this with a mocked 401 /api/info: 3 reloads in 3 minutes.
Could this only act when the 401 clearly comes from the proxy? For example: no UnauthorizedError JSON body, or a Location header like Authelia sends (#57649), or a check like #52223's /api/info probe.
This removes the service worker for good, so offline/PWA support is gone until it registers again. #52223 gets the same result without removing it: it adds a query parameter (reauth) and keeps that parameter out of the service worker's navigation fallback in vite.pwa.ts, so that one navigation goes to the network.
Going to a hardcoded / drops the route the user was on and breaks when the app is served under a path prefix. Reloading the current URL with an extra query parameter would keep both.
There was a problem hiding this comment.
Addressed in e902caf:
- OpenCode UnauthorizedError JSON is recognized using the client guard and returned unchanged; its body remains readable and the password sign-in flow does not trigger any recovery navigation.
- Recovery keeps the current path, query, and hash, including a deployment prefix. Prefixed API paths and health checks are recognized.
- The worker stays installed. Only navigations containing reauth bypass the navigation fallback, and the app entry removes that parameter after returning. This follows the convention in fix(app): recover when a sign-in proxy session expires #52223; the Authentik callback-path exclusion is not part of this PR.
- Split the parameterized unit test into five focused cases. Added a real Chromium flow using the production wrapper and generated service worker behind a simulated cross-origin sign-in proxy. It verifies return to the original route, preserved worker registration, and a successful API call after login. Removing the reauth exclusion makes that test fail.
Validation: five unit cases, all 11 fixture-based service-worker tests, and bun run check pass. This is a simulated proxy flow, not verification against a live Authentik/Authelia deployment.
I reviewed #52223 and aligned the worker bypass and query cleanup with it. The fetch wrappers still overlap and should be consolidated before both PRs land; this variant handles proxy 401s as well as manual API redirects, without treating OpenCode password challenges as proxy expiry.
There was a problem hiding this comment.
Thanks for the update. Recovery now stays off after one attempt until /api/info succeeds again. I re-ran my probe of a password-protected opencode serve returning 401: the reload every 60s is gone and it now reloads once. The PR's test and packages/app typecheck pass. I still couldn't test the real SSO flow.
Public API: unchanged.
The earlier thread is still open. A 401 from OpenCode's own password check still costs one reload per tab, which drops the user's route. The service worker is still removed on every recovery. The reload still goes to a hardcoded /. The new ^/api check also means recovery quietly does nothing when the app is served under a path prefix.
The test has grown into one 72-line parameterised case, including a hand-built opaqueredirect response. It's hard to follow. A few small cases would be clearer: 401 triggers one reload, opaque redirect triggers one reload, other-origin and non-/api requests are ignored, and a healthy /api/info re-arms recovery.
It's also still worth coordinating with #52223, which handles the same expiry problem and also sets platform.fetch.
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
There was a problem hiding this comment.
My earlier points are fixed. OpenCode's own UnauthorizedError 401 is now passed back without reloading. Recovery no longer removes the service worker, because reauth=1 is excluded from the worker's cached-page fallback. It also keeps the current path, prefix, query and hash. I checked with a probe: a password-protected server polled for three minutes now gets no reloads, and a plain proxy 401 reloads once to /prefix/session/abc?x=1&reauth=1#h. The unit tests (5 cases) and packages/app typecheck pass. I couldn't run the Playwright service-worker test or a live SSO proxy here.
Public API: unchanged.
This now changes the same files as #52223 (entry.tsx, vite.pwa.ts, web.ts) and uses its reauth marker. As you note, one of the two should be rebased onto the other or they should be combined before both land.
|
I hit the same failure on a live Authelia deployment, and I can confirm the Environment:
Observed after the Authelia session expires (4h inactivity / 24h expiration):
This matches #50481 / #57649. I have not tested the PR build yet, but this deployment can be used to verify the live Authelia path, including the one-shot One request: please keep the |
|
@Hamza-Abili can you confirm if adding an exception for /authelia is a safe assumption? See the updated description above. Apparently it's user configurable but I expect most use the default suggestion. |
|
@chetan Thanks for the follow-up. I checked this against Authelia's configuration model.
In my deployment the portal is on a separate origin ( So: adding Minor naming point: the |
Issue for this PR
Related reports: #50481 and #57649. Related PR: #52223.
Type of change
What does this PR do?
When a whole-site SSO proxy session expires, API requests fail. The service worker can still supply the cached app. The browser does not reach the proxy to sign in again.
API requests to the app origin now use manual redirects. A proxy redirect or
401response starts one navigation to the current URL withreauth=1. This parameter prevents the worker from supplying cached HTML for that navigation. The worker stays installed. The route, query, hash, and deployment prefix stay in the recovery URL. The app removes the parameter after sign-in.An expired session can also cause a lazy CSS or JavaScript file to fail. The browser loads these files without the API fetch wrapper. The app now listens for
vite:preloadErrorand checks/api/infothrough the same wrapper. The check has a 10-second time limit. Only a proxy auth failure starts recovery. Missing files and offline failures keep their original errors.OpenCode
UnauthorizedErrorresponses stay unchanged, so its password sign-in screen still works. Recovery stays disabled until an API health check succeeds after the return from sign-in. This prevents repeated reloads. Requests to other origins and the native desktop client are unchanged. Web tabs in desktop browsers use the new handling.We prefer this PR to #52223 for whole-site SSO proxies. This PR handles proxy
401responses and redirects directly. It also checks auth after browser CSS and JavaScript load failures. #52223 starts its check only when a wrapped fetch rejects. It does not directly handle401responses or browser asset load failures. Both PRs usereauth=1; use one recovery implementation rather than both wrappers.This change does not handle login pages returned as HTTP
200, or proxies that protect only API paths.The service worker also leaves common SSO proxy paths to the network:
/outpost.goauthentik.io,/oauth2,/.pomerium,/_oauth,/cdn-cgi,/.auth, and/authelia, including their subpaths and query strings. This lets Authentik, OAuth2 Proxy/AWS ALB, Pomerium, Traefik Forward Auth, Cloudflare, Azure Easy Auth, and Authelia handle their own callbacks and portals instead of loading cached OpenCode HTML on an unsupported route. Exact path boundaries keep similarly named app routes unaffected. The extended browser regression reproduced the interception before the fix; all 15 service-worker fixture tests passed afterward, and the updated case passed 10 runs with five workers and no retries. For this follow-up,bun run checkwas blocked by missing dependencies and type errors in unchanged files; the pre-push hook encountered the same failures and was bypassed.How did you verify your code works?
bun run checkpassed.bun run typecheck:e2ereported nine errors in unchanged regression and performance test files. It reported no errors in the changed files.The recovery flow has not been tested against live Authentik or Authelia.
Screenshots / recordings
No visual controls changed. Recovery uses a full-page navigation.
Recovery flow
Visual
Checklist
Added by the opencode bot (AI-generated).
Related: #57649, #50481