Skip to content

Blank window when organization.list fails while auth is healthy #6794

Description

@ashbrener

Context

When organization.list fails but the auth endpoints are healthy, a returning window renders nothing at all — no message, no retry, indistinguishable from a hang. CollectionsProvider wraps the whole authenticated tree and returns null until it can resolve the window's org, and it deliberately waits for the membership list before deciding. That wait is correct; the blank screen it produces during an outage is not.

This is not the offline case. An offline launch is already handled: the session is in-memory only, so get-session fails and _authenticated/layout.tsx:244 shows "You're offline" with a Retry. The bug needs /api/auth/* to succeed while /api/trpc does not.

Steps

  1. Open the app once and let a window persist its org to the window registry.
  2. Make /api/trpc fail while /api/auth/* keeps serving (bad tRPC deploy, rate limit, or a DB fault in the organizations ⋈ members join — better-auth's cookieCache at packages/auth/src/server.ts:135-138 lets sessions answer without touching those tables).
  3. Relaunch.

Expected: an error state explaining the app can't reach the server, with a way to retry.
Actual: an empty window.

Implementation notes

Files

  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx:112if (windowOrgId != null && organizations == null) return; blocks initialization. The comment above it reasons "the window is showing nothing yet either way", which holds for a few hundred milliseconds and not for an outage.
  • .../CollectionsProvider.tsx:206if (!contextValue) return null; blanks the entire authenticated subtree (_authenticated/layout.tsx:289-336), including <Outlet />, the sidebar and the window-drag regions.
  • apps/desktop/src/renderer/providers/ElectronTRPCProvider/ElectronTRPCProvider.tsx:48hostServiceQueryRetry governs cloud queries too, despite the name. It only retries when error.data == null, so a 500 through tRPC gets zero retries; only transport failures retry (3x, ~4.2s total).

Approach

Keep the guard exactly as it is — it exists so a dead registry org can't pin the window to an org whose every read 403s, and that invariant is worth more than the blank screen costs. Instead, distinguish "still loading" from "gave up" at the render site: when the org is unresolved and organization.list is in error, render an error state with a Retry wired to the query's refetch() rather than null.

_authenticated/layout.tsx:204-259 already has both panels to copy — the offline card and the 15s "Still restoring your session" escape hatch built on useDelayElapsed (:99-102). Neither is applied to org resolution today.

Gotchas

  • Do not fall back to the registry org on error. initializedRef (:98/:116) is one-shot: adopting an unverified org id on a transient failure means that when the query later succeeds and reveals the id is dead, nothing re-evaluates. That pins the window permanently and is strictly worse than the blank, which does recover.
  • The blank is not permanent today. refetchOnWindowFocus/refetchOnReconnect are at their defaults and organizations is in the effect's dependency list, so it self-heals on focus or reconnect. The harm is the silence, not the persistence.
  • There are no component tests for CollectionsProvider, and the app has exactly one React component test overall (components/Redirect/Redirect.test.tsx). Extracting the resolution into a pure function next to evictInactiveOrgs.ts would match the five existing co-located unit tests and let the decision table be tested without a DOM.
  • Introduced by feat(desktop): multi-window with per-window organization context (#4018) #5337 (b9d851f93), which added per-window org context.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions