Fix "Unsupported Version" Block on Valid Servers After App Update - #3323
Conversation
…rvers After a macOS app update, opening a supported server (e.g. open.rocket.chat running 8.5) could show the "unsupported version" block screen until the app was quit and restarted. Root cause: the renderer's SupportedVersionDialog ran isServerVersionSupported on persisted Redux state while the main process was still fetching /api/info. Between the version-update and supportedVersions-update dispatches, the renderer observed mismatched state and wrote isSupportedVersion: false. The 30-min throttle then locked the wrong verdict in until restart. Fix the race by making the main process the sole authoritative writer of WEBVIEW_SERVER_IS_SUPPORTED_VERSION, computed against the freshly-fetched version and uniqueId. The verdict dispatches before the supportedVersions UPDATED action so the UI never sees a fresh fetchState='success' paired with stale verdict. Additional hardening: - Cache and builtin fallback paths now compute and dispatch a verdict too, so degraded networks (firewalled enterprises, cloud outage) still enforce. - No-data path preserves any prior definitive verdict (security-correct fail-secure when /api/info, cloud, cache, AND builtin all fail). - Per-URL request generation guard: overlapping calls from WEBVIEW_READY / RELOADED / DISMISS / refresh-supported-versions can no longer last-writer-overwrite each other's verdict or poison the cache. - getUniqueId endpoint selection uses the freshly-fetched /api/info version so a pre-7.0.0 persisted version with a fresh 7.0.0+ server no longer hits the legacy settings endpoint. - Exception matching adds exact-string and commit-hash (sha-<7chars> + full hash) lookup before semver, so per-tenant SHA exceptions like sha-bb83777 actually match. - Exception scope is enforced: a payload's exceptions.domain and exceptions.uniqueId must match the local server's hostname and uniqueID; missing local identity rejects (prevents cross-tenant bypass via the bundled builtin payload). - WEBVIEW_SERVER_VERSION_UPDATED carries gitCommitHash from /api/info and persists it on Server state; the reducer preserves the existing hash when a payload (e.g. preload's setVersion) omits it. The SupportedVersionDialog now passes server.gitCommitHash so sha-based exceptions are evaluated consistently across renderer and main. - The renderer dialog no longer dispatches WEBVIEW_SERVER_IS_SUPPORTED_VERSION, removing the renderer/main race entirely. UnsupportedServer keeps blocking on a definitive false verdict except during active loading, so persisted unsupported state survives degraded fetches without reverting to the loading-only check that originally allowed the race window. Adds 60+ unit tests covering: server/cloud/cache/builtin verdict dispatch ordering, fresh-version usage on every path, overlapping-request guard, cache poisoning guard, tenant scope (cross-domain, cross-uniqueId, missing-uniqueID rejection), commit-hash exception matching, gitCommitHash preservation, and no-data preservation behavior.
Captures the timeline of the fix in 9 review rounds, the underlying patterns (renderer/main race, stale-request guards, scope enforcement, shared-action payload erosion, fail-secure under missing evidence), and the limits of the change. Cross-referenced from project lessons memory.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📜 Recent review details⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
WalkthroughIntroduces identity-aware, concurrency-guarded supported-version checks: per-URL generation gating, scoped exception matching (hostname + uniqueID + optional commit hash), expanded multi-source evaluation (server, cloud, cache, builtin), payload enrichment with gitCommitHash, extensive tests, a post-mortem, and a small CI/workflow tweak. ChangesSupported-Versions Race Condition Hardening
Sequence Diagram(s)sequenceDiagram
actor Renderer as Renderer (UI)
participant Main as Main Process
participant Cloud as Cloud / Supported-Versions CDN
participant Cache as Local Cache / ElectronStore
Renderer->>Main: request isServerVersionSupported(server, gitCommitHash?)
Main->>Main: increment requestGeneration for server URL
Main->>Main: fetch /api/info (uniqueID) or use cached uniqueID
alt server-source available
Main->>Main: evaluate server-provided supported-versions against identity
else cloud-source
Main->>Cloud: fetch supported-versions
Cloud-->>Main: supportedVersions payload
Main->>Main: decode, validate, compare with identity
Main->>Cache: write guarded cache entry (if not stale)
else cache/builtin fallback
Main->>Cache: read cached supported-versions
Main->>Main: evaluate fallback against identity (preserve prior verdicts if absent)
end
Main-->>Renderer: dispatch WEBVIEW_SERVER_VERSION_UPDATED { url, version, gitCommitHash? }
Main-->>Renderer: (optionally) dispatch WEBVIEW_SERVER_IS_SUPPORTED_VERSION { url, isSupported }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested labelstype: bug 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The macOS smoke step launches the unsigned `--dir` build of the app for 30s and accepts either a clean exit (0) or a gtimeout-reached exit (124). Recently the helper process has been intermittently exiting 133 (SIGTRAP) after the app has already initialized — driven by the Hardened Runtime + XPC sandbox tripping on the unsigned helper bundle (visible in logs as `com.apple.backupd.sandbox.xpc: Connection invalid`). The app itself is healthy at that point. Accept 133 as non-fatal alongside 0/124, with a CI warning so a real regression wouldn't be silenced. SIGSEGV/SIGABRT/etc. still fail the step.
Linux installer download |
macOS installer download |
…ons-race # Conflicts: # src/servers/supportedVersions/main.main.spec.ts # src/servers/supportedVersions/main.ts
…3404) The exception scope check introduced in #3323 requires exceptions.uniqueId to equal server.uniqueID, but the server-signed validation path returns before getUniqueId() ever runs and /api/info does not include a uniqueId field, so the local uniqueID is missing on that path and uniqueId-scoped exceptions were always disqualified. Resolve the workspace uniqueID from the server before validating when the payload carries a uniqueId-scoped exceptions block, persist it via WEBVIEW_SERVER_UNIQUE_ID_UPDATED so subsequent runs (including offline cache validation) keep working, and keep rejecting when the fetched value does not match. Also compare exceptions.domain case-insensitively (DNS names are case-insensitive per RFC 4343).
* fix: honor uniqueId-scoped support exceptions on server-signed path The exception scope check introduced in #3323 requires exceptions.uniqueId to equal server.uniqueID, but the server-signed validation path returns before getUniqueId() ever runs and /api/info does not include a uniqueId field, so the local uniqueID is missing on that path and uniqueId-scoped exceptions were always disqualified. Resolve the workspace uniqueID from the server before validating when the payload carries a uniqueId-scoped exceptions block, persist it via WEBVIEW_SERVER_UNIQUE_ID_UPDATED so subsequent runs (including offline cache validation) keep working, and keep rejecting when the fetched value does not match. Also compare exceptions.domain case-insensitively (DNS names are case-insensitive per RFC 4343). * fix: harden supported-versions validation and recovery paths Audited the supported-versions subsystem end to end after the uniqueId-scope escalation and fixed the confirmed defects: - Wrap the cache/builtin fallback validation in a helper with try/catch so a malformed cached payload can no longer reject the update before the error state is dispatched, which left fetchState stuck at 'loading' and suppressed the UnsupportedServer block gate. - Catch rejections at all four fire-and-forget validation call sites. - Fall back to the persisted workspace uniqueID when the fresh fetch fails so the cloud lookup is not skipped for previously-known servers. - Switch uniqueID/version/gitCommitHash reducer cases from upsert to update so a late identity dispatch cannot resurrect a server deleted while a validation was in flight. - Guard getExpirationMessageTranslated against payloads whose i18n dictionary lacks both the user language and 'en'; the missing guard crashed the async check and silently suppressed the expiring- workspace warning dialog. - Revalidate all servers on powerMonitor 'resume' (the window 'online' event does not fire when waking with the same network connected). - Use the real currentView state in SupportedVersionDialog's effect dependencies; the previous dependency was the imported reducer function, which never changes, so the dialog never re-checked on view switches. - Add a 'Check again' button to the unsupported-workspace screen so users can re-trigger validation without restarting the app. - Fix logRequestError printing a literal ${description}. Also relax the exception scope check for UNKNOWN local identity: an unfetchable workspace uniqueID (e.g. settings.public restricted by enterprise API ACLs) no longer disqualifies a domain-matched exception; a PROVEN uniqueID mismatch still rejects. The gate is client-side UX enforcement rather than a security boundary, and wrongly blocking a legitimate workspace is the worse failure mode. * fix: honor tenant-scoped exceptions from self-scoped payload sources Server, cloud, and cache supported-versions payloads are fetched from or for the server being validated, so their exceptions block cannot belong to another tenant. For these sources a domain/uniqueId scope mismatch is now logged as a diagnostic warning instead of disqualifying the exception. The bundled builtin payload is the only source that could carry another deployment's exceptions and keeps the strict scope requirement. This removes the remaining paths where a valid, unexpired exception could be rejected over unverifiable or drifted identity data (restricted settings.public API, rotated workspace uniqueID, stale persisted state). * fix: do not block when enforcementStartDate is missing or malformed A missing or unparseable enforcementStartDate previously produced an Invalid Date that failed the future-date comparison and fell through to the unsupported verdict — blocking the workspace based on incomplete payload data. Blocking now requires a valid, past enforcement date: uncertain data keeps the server usable until a payload with a valid enforcement date proves enforcement is active. * fix: align supported-versions types and fixtures with the real /api/info contract; try both fallback sources The desktop's ServerInfo type declared fields the server never returns to unauthenticated callers (verified against the server source, apps/meteor/server/api/lib/getServerInfo.ts): - Remove the fictional uniqueId field. No server version includes it in /api/info; the workspace uniqueID comes from settings.public and is resolved on demand by withExceptionScopeUniqueId. Remove the dead serverInfoResult.uniqueId fallback in updateSupportedVersionsData. - Mark build, marketplaceApiVersion, and commit optional and document that they only appear for authenticated view-statistics callers; the desktop always calls /api/info unauthenticated. The sha-exception path uses the persisted server.gitCommitHash pushed by the injected script instead. - Rebuild the default test fixture to mirror the real unauthenticated wire shape (trimmed major.minor version, no uniqueId/commit/info) so the suite can no longer validate assumptions against fields real servers do not send — the gap that let the exception-scope defect pass CI. Switch beforeEach to jest.resetAllMocks(), fixing latent cross-test mock leakage the honest fixtures surfaced. Also rework the offline fallback to try BOTH remaining sources instead of picking one by timestamp: the bundled builtin payload never carries tenant exceptions, so preferring a fresher builtin over a cache that holds the tenant's exception blocked a workspace that should stay usable. Candidates are checked in freshness order and the first source that supports the server wins; blocking requires every available source to fail. This keeps the stale-cache rescue from #3388 while never losing a cached exception to it. * test: add live /api/info wire-contract spec Asserts the unauthenticated /api/info response shape against a real server (open.rocket.chat): trimmed major.minor version, no uniqueId, no commit/info blocks, string minimumClientVersions, JWT-shaped supportedVersions.signed. Skips only on genuine network failure or SKIP_CONTRACT_TESTS=1, so contract drift between the desktop and the server cannot hide behind mocks that agree with themselves. * fix(CORE-2400): remove startup screen capture enumeration that opens Wayland picker (#3308) (#3400) * chore: bump version to 4.15.3
Fix "Unsupported Version" Block on Valid Servers After App Update
Summary
Resolves a bug where the desktop app could show the "Workspace version unsupported" block screen on a perfectly valid Rocket.Chat workspace right after updating the desktop app. Quitting and relaunching the app cleared the block, but the disruption was confusing for end users and made the workspace appear broken when it was not. After this change the desktop app correctly recognizes supported workspaces on first launch, including workspaces that rely on per-instance support exceptions.
What's New
Fixed
Improved
Platform Notes
This fix is platform-agnostic. The bug was reported on macOS (typically right after updating the desktop app), but the underlying behavior was the same on Windows and Linux, so all three platforms benefit from the fix.
How to Test
Recognized supported workspace on first launch
https://open.rocket.chat).Per-workspace exception is honored
Spotty connectivity does not flip support state
Genuinely unsupported workspace still blocks
Related
https://open.rocket.chatafter the v4.14.1 update.Summary by CodeRabbit
Bug Fixes
Documentation
Tests
Chores