Connection: Don't full-reconnect on inconclusive validation when the blog token is healthy - #52851
Conversation
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! Jetpack plugin: The Jetpack plugin has different release cadences depending on the platform:
If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Backup plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Boost plugin:
If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Search plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Social plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Protect plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Videopress plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. Stats Data plugin: No scheduled milestone found for this plugin. If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
Code Coverage SummaryCoverage changed in 2 files.
|
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The routing change is narrowly scoped, preserves existing fallback behavior, and has comprehensive regression coverage.
Review effort: Balanced
Findings: None
What changed in this PR
Prevents destructive full reconnects when only the owner token is missing and ensures successful token replacement clears stale connection errors.
Changes:
- Independently validates the blog token before choosing full reconnection.
- Emits the user-token update action from the shared write path.
- Adds routing, hook-wiring, and error-clearing tests plus user-facing changelogs.
| File | Description |
|---|---|
projects/packages/connection/src/class-manager.php |
Adds fallback blog-token validation during restore. |
projects/packages/connection/src/class-tokens.php |
Emits the token-updated action for every write path. |
projects/packages/connection/src/class-rest-connector.php |
Removes the redundant action emission. |
projects/packages/connection/tests/php/ManagerTest.php |
Tests restore routing across token-health states. |
projects/packages/connection/tests/php/TokensTest.php |
Tests token-update action arguments. |
projects/packages/connection/tests/php/Error_Handler_Test.php |
Tests stale-error removal after token writes. |
projects/packages/connection/changelog/fix-restore-inconclusive-validation |
Documents the package fix. |
projects/plugins/backup/changelog/fix-restore-inconclusive-validation |
Documents the Backup-visible fix. |
projects/plugins/boost/changelog/fix-restore-inconclusive-validation |
Documents the Boost-visible fix. |
projects/plugins/jetpack/changelog/fix-restore-inconclusive-validation |
Documents the Jetpack-visible fix. |
projects/plugins/protect/changelog/fix-restore-inconclusive-validation |
Documents the Protect-visible fix. |
projects/plugins/search/changelog/fix-restore-inconclusive-validation |
Documents the Search-visible fix. |
projects/plugins/social/changelog/fix-restore-inconclusive-validation |
Documents the Social-visible fix. |
projects/plugins/stats/changelog/fix-restore-inconclusive-validation |
Documents the Stats-visible fix. |
projects/plugins/videopress/changelog/fix-restore-inconclusive-validation |
Documents the VideoPress-visible fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
coder-karen
left a comment
There was a problem hiding this comment.
Thank you, this tests well for me. I left a few nit comments, no blockers.
…blog token is healthy Manager::restore() collapsed every non-parseable Tokens::validate() result into "both tokens broken" and escalated to reconnect() — deregistering the site and disconnecting every user — even when the blog token was perfectly healthy (e.g. a deleted owner token). Check the blog token on its own in that branch, and when it is confirmed healthy route to the surgical refresh_user_token() -> 'authorize' path instead. Everything below the branch is unchanged. Fixes CONNECT-455. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYDknke15qyKNtrT2KMcyo
- Add @SInCE to the restore() docblock for the inconclusive-validation blog check. - Trim the else-branch comment and note that user_token_healthy = false is "unknown, treated as unhealthy". - Collapse the restore() flag tests into two data providers; assert validate_blog_token() is never called on the conclusive branch and once on the inconclusive branch (pins the extra request to that branch); add WP_Error and malformed-array inconclusive inputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYDknke15qyKNtrT2KMcyo
… REST endpoint Every user-token write funnels through Tokens::update_user_token(), but only the REST endpoint announced the write via jetpack_updated_user_token. The other paths (authorize() — the owner-reconnect behind "Restore Connection" — plus provisioning and CLI) replaced the token silently, so Error_Handler::delete_all_errors() never ran and a stale broken-connection notice lingered until a second reconnect. Move the action into Tokens::update_user_token() so every write path clears stale errors, and drop the now-redundant do_action in the REST endpoint. Fired unconditionally, matching the endpoint's prior behavior: Jetpack_Options::update_options() returns void, so there is no success flag to gate on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYDknke15qyKNtrT2KMcyo
…per project Both reconnect fixes in this PR (surgical restore() routing and clearing the stale error on the first reconnect) touch the same "Restore Connection" flow, so a single changelog entry per project describes them together instead of two separate files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYDknke15qyKNtrT2KMcyo
c0fa085 to
e498016
Compare
…ding) Remove the now-stale "trigger a full reconnection" comment above the validate() branch (the else no longer always full-reconnects), and tighten the package changelog entry per review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYDknke15qyKNtrT2KMcyo
Fixes CONNECT-455
Proposed changes
This PR makes two complementary fixes to the owner-reconnect ("Restore Connection") flow for the deleted-owner-token case — the one the
invalid_connection_owner/has_user_token: falsenotice is shown for. The first makes the reconnect surgical instead of destructive; the second makes the stale notice clear on the first attempt instead of the second. Neither depends on the other, but they land together because they fix the same user-visible flow.1.
restore()no longer full-reconnects when only the owner's user token is missingManager::restore()(the backend of the "Restore Connection" CTA) escalated to a fullreconnect()—disconnect_site_wpcom( true )(jetpack.deregister, revoking every user connection on WordPress.com) plusregister()(wiping the localuser_tokensoption) — wheneverTokens::validate()failed to return health flags. Butvalidate()returns a non-parseable result in several cases that say nothing about token health: the current user's token is missing locally (the deleted-owner-token case, which bails before ever contacting WordPress.com), the blog token is missing locally, a transient transport failure / non-200, or a malformed response body.restore()collapsed all of these intoblog_token_healthy = false; user_token_healthy = false, which satisfied the equal-flags branch and triggered the full teardown. The escalation was theelseof a parsing check, not a decision.Concretely: an owner whose token was deleted (blog token perfectly healthy) clicked "Restore connection" and every healthy secondary user got disconnected — while the notice copy we ship for exactly that state (
invalid_connection_owner,has_user_token: false) promises "reconnect your WordPress.com account", a surgical fix.This PR replaces the collapsing
elsewith an independent blog-token check:Tokens::validate_blog_token()signs with the blog token only (usable precisely when the user token is missing) and returns stricttrueonly on a confirmed-healthy response — as of #52718 it returns aWP_Errorwhen the check can't be performed, sotrue === …treats a transient/unverifiable check as "not healthy" and its own failures can't masquerade as health. When it confirms health, the existing unequal-flags logic routes torefresh_user_token()→'authorize', minting a fresh user token without needing the old one. Everything below the branch is unchanged.Behavior matrix
'authorize'(caller re-links; blog token and other users untouched)validate_blog_token()can't sign, returns non-true)validate()'authorize'(strictly less destructive)This is the low-risk diagnosis fix that CONNECT-381 Phase C's caller-aware routing depends on: Phase C's "only the user token is unhealthy (blog token healthy)" condition wasn't evaluable in the deleted-owner-token case, because
restore()never learned the blog token was healthy. This slice fixes the diagnosis only — it touches no ownership semantics, needs no WordPress.com changes (jetpack-token-health/blogalready exists), and routes to the existing'authorize'path.2. Owner reconnect now clears the stale connection error on the first attempt
With fix #1 in place, reconnecting the owner via
authorize()mints a fresh user token — but the pre-existinginvalid_connection_ownererror stayed stored, so the broken-connection notice lingered until a second reconnect. That is a plain single-node bug, not a race:Error_Handlerclears stored errors by listening onjetpack_updated_user_token(→delete_all_errors()).Tokens::update_user_token(), but only the REST user-token endpoint firedjetpack_updated_user_token.authorize()(the owner-reconnect path), provisioning, and WP-CLI all wrote the token silently, so the listener never ran and the error persisted.disconnect_user()finally succeeded and firedjetpack_unlinked_user(which also clears errors). Reproducible on a single-node site with no object cache and no concurrency.The fix moves the
do_action( 'jetpack_updated_user_token', $user_id, $token )down intoTokens::update_user_token()— the one function every write path funnels through — and drops the now-redundant call in the REST endpoint so it doesn't double-fire. It fires unconditionally, matching the endpoint's prior behavior:Jetpack_Options::update_options()returnsvoid, so there is no success flag to gate on. Manager's memoized owner id is already reset on any user-token write (reset_connection_status()is hooked topre_update_jetpack_option_user_tokens), so no extra bookkeeping is needed.Net effect:
authorize(), provisioning, and CLI now clear stale connection errors exactly as the REST endpoint always did — so the owner reconnect resolves the notice on the first attempt.Related product discussion/links
invalid_connection_ownernotice persisted after the first reconnect)Does this pull request change what data or activity we track or use?
No.
Testing instructions
Unit tests —
jp test php packages/connectionrestore()suite inManagerTest(partial-mockedManager, spying onreconnect()) covers the full matrix above:validate()+ confirmed-healthy blog token ⇒reconnect()never called, returns'authorize';reconnect()called (unchanged);WP_Error) ⇒reconnect()called (not treated as healthy);TokensTest::test_update_user_token_fires_action— a user-token write firesjetpack_updated_user_tokenwith($user_id, $token).Error_Handler_Test::test_user_token_write_clears_stored_owner_error— the end-to-end guarantee: a storedinvalid_connection_ownererror is gone after a user-token write, via the productionjetpack_updated_user_token→delete_all_errors()wiring.Manual (a Jetpack-connected self-hosted site, e.g. Jurassic Ninja)
invalid_connection_owner/has_user_token: falsestate (e.g. clear the owner's row from theuser_tokensoption), leaving the blog token intact.Cost note: one extra signed GET (
jetpack-token-health/blog), only in the previously-inconclusive branch of a user-initiated restore.