You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Editor capability detection (EditorSettingsRepository.fetchEditorCapabilitiesForSite) runs from multiple call sites with different race profiles, and the My Site connectivity banner coordinates it against the asynchronous application-password mint by hand.
Proposal: make capability detection a single per-site state, owned by one component and exposed as a StateFlow, that owns its async preconditions. Every consumer — connectivity banner, editor preloader, EditorCapabilityResolver, application-password card — becomes a subscriber that renders its slice of that state.
Background
#22883 changed the Atomic route-support probe to run REST API autodiscovery against the site's own host instead of the WP.com proxy. #22926 fixes the resulting regression — a false "Unable to connect to your site" banner on private Atomic sites, whose host gates the unauthenticated discovery request — by falling back to an authenticated app-password probe.
The core fix is small. The complexity is everything added to make it work on first login, where the application password is minted asynchronously on the My Site screen and the capability fetch races it:
suppress the banner while credentials are pending (isAwaitingApplicationPassword),
a re-detect signal when credentials are established (ApplicationPasswordMonitor),
re-surface the banner when the mint fails terminally (hasMintFailed).
Adversarial review of #22926 found two defects in that layer:
ApplicationPasswordViewModelSlice calls onMintFailed for every non-success, including transient failures. OnApplicationPasswordCreated carries notSupported (terminal vs. transient), but it's ignored — so a transient first-login mint failure latches hasMintFailed = true and shows the exact false banner the change set out to suppress.
The re-detect collector reads SelectedSiteRepository.getSelectedSite() — an in-memory instance refreshed by an asyncOnSiteChanged EventBus callback, with no ordering vs. the credential-established signal — so it can observe a credential-less SiteModel.
Both are symptoms of the same thing: capability detection and its preconditions are spread across components that coordinate at a distance.
Root cause
The connectivity banner uses fetchEditorCapabilitiesForSite() as a reachability probe. For Atomic sites that operation has two async preconditions provisioned elsewhere:
an application password (minted by ApplicationPasswordViewModelSlice / ApplicationPasswordLoginHelper), and
a recovered wpApiRestUrl (SiteApiRestUrlRecoverer).
The same "detect editor capabilities for this site" operation is implemented or triggered in three places, each with a different race profile:
SiteConnectivityBannerViewModelSlice.fetchCapabilities — for the banner; gated once-per-site.
GutenbergEditorPreloader.preloadIfNeeded — for editor preload; recovers wpApiRestUrl itself, then calls fetchEditorCapabilitiesForSite; gated once-per-site.
ApplicationPasswordViewModelSlice.healApiRestUrlIfMissing — recovers the wpApiRestUrl the other two depend on.
There is no single owner, no single result, and no single point that observes credential changes — so each consumer re-derives state and they coordinate via ApplicationPasswordMonitor.
Proposed approach
1. Separate two domains
Site auth / provisioning state — does this site have working application-password credentials? Owned by an application-password repository; consumed by the application-password card.
Editor capability state — given a usable auth context, what does the REST API support? Owned by editor-capability detection; consumed by the editor.
The connectivity banner is a derived view of both, which is why it kept tangling with the auth lifecycle.
2. Model each as a typed state, not a boolean
// Auth domain — fed by ONE FluxC observersealedinterfaceSiteAuthState {
object NotApplicable // non-Atomic / already REST-authed
object Provisioning // mint in flight <- pending, NOT failure
object Provisioned // creds usable
object Unprovisionable // terminal: not supported / mint failed
}
// Capability domain — a function of (site, auth)sealedinterfaceEditorCapabilityState {
object Pending // waiting on auth, or probing
data classReady(valcaps:EditorCapabilities)
object Unreachable // creds present, transport probe failed
object TransientError // retry next resume
}
The transient-vs-terminal distinction (Provisioning vs. Unprovisionable) is now a type, so the #22926 false-banner bug is unrepresentable.
The flow is the notification — late subscribers and config changes get the current value for free. This replaces ApplicationPasswordMonitor (no replay buffer, no failed-id set), the two ad-hoc once-per-site caches, the duplicate wpApiRestUrl heal, and the getSelectedSite() re-read (one FluxC observer; no consumer touches SiteModel staleness).
4. Consumers become subscribers
Combined state
Connectivity banner
Application-password card
Editor
auth=Provisioning, or cap=Pending/TransientError
hidden
hidden
wait
auth=Unprovisionable
hidden
auth prompt
—
cap=Unreachable
show
hidden
—
cap=Ready
hidden
hidden
use caps
SiteConnectivityBannerViewModelSlice, GutenbergEditorPreloader, EditorCapabilityResolver, and the application-password card all collect from the same source.
One detector → the preloader and banner can't disagree or double-probe, and the duplicate URL heal goes away.
Migration plan
Fix false connectivity banner on private Atomic sites #22926 (release/26.8): land the minimal fix — authenticated fallback + isAwaitingApplicationPassword suppression — and roll back the ApplicationPasswordMonitor hardening. Low-risk regression fix.
Introduce EditorCapabilityState + EditorCapabilityDetector; migrate SiteConnectivityBannerViewModelSlice to collect it.
Migrate GutenbergEditorPreloader and EditorCapabilityResolver onto the same source (removes the duplicate probe + heal); migrate the application-password card onto the auth StateFlow.
Wire EditorCapabilityDetector.clear() into the existing sign-out reset (AppInitializer.removeWpComUserRelatedData, which already calls clearAllClients()).
Out of scope
A general-purpose feature-detection framework — scope this to the four consumers above.
Summary
EditorSettingsRepository.fetchEditorCapabilitiesForSite) runs from multiple call sites with different race profiles, and the My Site connectivity banner coordinates it against the asynchronous application-password mint by hand.ApplicationPasswordMonitor) plus polling flags — is where the recurring banner bugs live (see Fix false connectivity banner on private Atomic sites #22926).StateFlow, that owns its async preconditions. Every consumer — connectivity banner, editor preloader,EditorCapabilityResolver, application-password card — becomes a subscriber that renders its slice of that state.Background
#22883 changed the Atomic route-support probe to run REST API autodiscovery against the site's own host instead of the WP.com proxy. #22926 fixes the resulting regression — a false "Unable to connect to your site" banner on private Atomic sites, whose host gates the unauthenticated discovery request — by falling back to an authenticated app-password probe.
The core fix is small. The complexity is everything added to make it work on first login, where the application password is minted asynchronously on the My Site screen and the capability fetch races it:
isAwaitingApplicationPassword),ApplicationPasswordMonitor),hasMintFailed).Adversarial review of #22926 found two defects in that layer:
ApplicationPasswordViewModelSlicecallsonMintFailedfor every non-success, including transient failures.OnApplicationPasswordCreatedcarriesnotSupported(terminal vs. transient), but it's ignored — so a transient first-login mint failure latcheshasMintFailed = trueand shows the exact false banner the change set out to suppress.SelectedSiteRepository.getSelectedSite()— an in-memory instance refreshed by an asyncOnSiteChangedEventBus callback, with no ordering vs. the credential-established signal — so it can observe a credential-lessSiteModel.Both are symptoms of the same thing: capability detection and its preconditions are spread across components that coordinate at a distance.
Root cause
The connectivity banner uses
fetchEditorCapabilitiesForSite()as a reachability probe. For Atomic sites that operation has two async preconditions provisioned elsewhere:ApplicationPasswordViewModelSlice/ApplicationPasswordLoginHelper), andwpApiRestUrl(SiteApiRestUrlRecoverer).The same "detect editor capabilities for this site" operation is implemented or triggered in three places, each with a different race profile:
SiteConnectivityBannerViewModelSlice.fetchCapabilities— for the banner; gated once-per-site.GutenbergEditorPreloader.preloadIfNeeded— for editor preload; recoverswpApiRestUrlitself, then callsfetchEditorCapabilitiesForSite; gated once-per-site.ApplicationPasswordViewModelSlice.healApiRestUrlIfMissing— recovers thewpApiRestUrlthe other two depend on.There is no single owner, no single result, and no single point that observes credential changes — so each consumer re-derives state and they coordinate via
ApplicationPasswordMonitor.Proposed approach
1. Separate two domains
The connectivity banner is a derived view of both, which is why it kept tangling with the auth lifecycle.
2. Model each as a typed state, not a boolean
The transient-vs-terminal distinction (
Provisioningvs.Unprovisionable) is now a type, so the #22926 false-banner bug is unrepresentable.3. One detector, exposed as a
StateFlowThe flow is the notification — late subscribers and config changes get the current value for free. This replaces
ApplicationPasswordMonitor(no replay buffer, no failed-id set), the two ad-hoc once-per-site caches, the duplicatewpApiRestUrlheal, and thegetSelectedSite()re-read (one FluxC observer; no consumer touchesSiteModelstaleness).4. Consumers become subscribers
auth=Provisioning, orcap=Pending/TransientErrorauth=Unprovisionablecap=Unreachablecap=ReadycapsSiteConnectivityBannerViewModelSlice,GutenbergEditorPreloader,EditorCapabilityResolver, and the application-password card all collect from the same source.Why this is better
getSelectedSite()race.Unprovisionablestate with a real owner (the card) → a feature, not an accidental gap (and it gives Allow headless application-password creation on Atomic sites #22884 a home).Migration plan
release/26.8): land the minimal fix — authenticated fallback +isAwaitingApplicationPasswordsuppression — and roll back theApplicationPasswordMonitorhardening. Low-risk regression fix.EditorCapabilityState+EditorCapabilityDetector; migrateSiteConnectivityBannerViewModelSliceto collect it.GutenbergEditorPreloaderandEditorCapabilityResolveronto the same source (removes the duplicate probe + heal); migrate the application-password card onto the authStateFlow.EditorCapabilityDetector.clear()into the existing sign-out reset (AppInitializer.removeWpComUserRelatedData, which already callsclearAllClients()).Out of scope
Unprovisionablestate), but the card fix is separate.Related
wpApiRestUrldoesn't survive across launches on Atomic sites #22905 — Atomic REST root moved away from/wp-json