Summary
Some sites in the local DB have WP_API_REST_URL (the @Column-mapped SiteModel.mWpApiRestUrl) set to NULL despite having valid application-password credentials. GutenbergKitSettingsBuilder.buildPostConfiguration reads this field with a fallback:
val siteApiRoot = if (shouldUseWPComRestApi) {
WPCOM_API_ROOT
} else {
site.wpApiRestUrl ?: "${site.url}/wp-json/"
}
For standard hosts (e.g. *.wpcomstaging.com, most self-hosted setups) the fallback happens to construct the same URL discovery would have produced. For non-standard REST roots — plugins that rewrite the REST namespace, wp-json under a subdirectory, sites that report a different apiRootUrl during discovery — the fallback is wrong and the editor will 404 on /wp-block-editor/v1/settings.
Evidence
Observed during on-device testing of #22893. DB query against the dev device:
386|https://vanilla.wpmt.co |u=28|p=60|url=32 ✓
1157|https://jetpack.wpmt.co |u=28|p=60|url=32 ✓
1215|https://automatticwidgets.wpcomstaging.com |u=32|p=60|url=51 ✓
1331|https://jeremyseriousbusinesstesting.wpcomstaging.com|u=32|p=60|url= ✗ NULL
jeremyseriousbusinesstesting.wpcomstaging.com is an Atomic site with working credentials but a NULL WP_API_REST_URL. The editor still works because the fallback https://jeremyseriousbusinesstesting.wpcomstaging.com/wp-json/ happens to be correct — but this is a coincidence of the host pattern, not by design.
The root cause is upstream: somewhere between initial site fetch and application-password setup, wpApiRestUrl either never gets populated for some sites or gets cleared by an unrelated code path. A controlled experiment (manually populate URL → run wipe+mint → verify) confirmed the field survives cleanly through the validator hardening in #22893. So the clearing is conditional on a live-state condition that wasn't isolated in a single testing session.
Proposed fix
wordpress-rs already exposes WpLoginClient.apiDiscovery(siteUrl): ApiDiscoveryResult (used today by ApplicationPasswordLoginHelper). Use it in GutenbergEditorPreloader.preloadIfNeeded to populate wpApiRestUrl lazily when missing:
if (site.wpApiRestUrl.isNullOrEmpty()) {
when (val result = wpLoginClient.apiDiscovery(site.url)) {
is ApiDiscoveryResult.Success -> {
site.wpApiRestUrl = result.success.apiRootUrl.toString()
dispatcher.dispatch(SiteActionBuilder.newUpdateSiteAction(site))
}
else -> { /* log, fall through to existing fallback */ }
}
}
preloadIfNeeded runs in scope.launch(bgDispatcher) and is invoked on every buildDashboardOrSiteItems call, so this catches the missing URL before the editor opens. Idempotent, no UI impact.
Also worth considering: populate the URL on the headless-mint success path in ApplicationPasswordViewModelSlice so newly-minted sites get it too. The mint goes through the Jetpack tunnel which doesn't perform discovery — that's how new Atomic mints can end up without a URL in the first place.
Out of scope for this issue
- Root cause investigation into why specific sites' URLs end up NULL after first setup
- Whether the
EditorHTTPClient concurrent 401 during a validator wipe window is worth fixing — that's credential staleness, not URL.
Related
Summary
Some sites in the local DB have
WP_API_REST_URL(the@Column-mappedSiteModel.mWpApiRestUrl) set to NULL despite having valid application-password credentials.GutenbergKitSettingsBuilder.buildPostConfigurationreads this field with a fallback:For standard hosts (e.g.
*.wpcomstaging.com, most self-hosted setups) the fallback happens to construct the same URL discovery would have produced. For non-standard REST roots — plugins that rewrite the REST namespace,wp-jsonunder a subdirectory, sites that report a differentapiRootUrlduring discovery — the fallback is wrong and the editor will 404 on/wp-block-editor/v1/settings.Evidence
Observed during on-device testing of #22893. DB query against the dev device:
jeremyseriousbusinesstesting.wpcomstaging.comis an Atomic site with working credentials but a NULLWP_API_REST_URL. The editor still works because the fallbackhttps://jeremyseriousbusinesstesting.wpcomstaging.com/wp-json/happens to be correct — but this is a coincidence of the host pattern, not by design.The root cause is upstream: somewhere between initial site fetch and application-password setup,
wpApiRestUrleither never gets populated for some sites or gets cleared by an unrelated code path. A controlled experiment (manually populate URL → run wipe+mint → verify) confirmed the field survives cleanly through the validator hardening in #22893. So the clearing is conditional on a live-state condition that wasn't isolated in a single testing session.Proposed fix
wordpress-rsalready exposesWpLoginClient.apiDiscovery(siteUrl): ApiDiscoveryResult(used today byApplicationPasswordLoginHelper). Use it inGutenbergEditorPreloader.preloadIfNeededto populatewpApiRestUrllazily when missing:preloadIfNeededruns inscope.launch(bgDispatcher)and is invoked on everybuildDashboardOrSiteItemscall, so this catches the missing URL before the editor opens. Idempotent, no UI impact.Also worth considering: populate the URL on the headless-mint success path in
ApplicationPasswordViewModelSliceso newly-minted sites get it too. The mint goes through the Jetpack tunnel which doesn't perform discovery — that's how new Atomic mints can end up without a URL in the first place.Out of scope for this issue
EditorHTTPClientconcurrent 401 during a validator wipe window is worth fixing — that's credential staleness, not URL.Related