Skip to content

Connection: Don't full-reconnect on inconclusive validation when the blog token is healthy - #52851

Merged
fgiannar merged 5 commits into
trunkfrom
fix/restore-inconclusive-validation-blog-token
Sep 29, 2026
Merged

fgiannar merged 5 commits into
trunkfrom
fix/restore-inconclusive-validation-blog-token

Conversation

@fgiannar

@fgiannar fgiannar commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

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: false notice 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 missing

Manager::restore() (the backend of the "Restore Connection" CTA) escalated to a full reconnect() — disconnect_site_wpcom( true ) (jetpack.deregister, revoking every user connection on WordPress.com) plus register() (wiping the local user_tokens option) — whenever Tokens::validate() failed to return health flags. But validate() 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 into blog_token_healthy = false; user_token_healthy = false, which satisfied the equal-flags branch and triggered the full teardown. The escalation was the else of 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 else with an independent blog-token check:

} else {
    // The paired health check could not run (a token is missing locally — e.g. a
    // deleted owner token — or the request failed): no evidence the blog token is
    // broken, and it's the one credential reconnect() would revoke for every user,
    // so check it on its own before that teardown.
    $blog_token_healthy = true === $this->get_tokens()->validate_blog_token();
    $user_token_healthy = false;
}

Tokens::validate_blog_token() signs with the blog token only (usable precisely when the user token is missing) and returns strict true only on a confirmed-healthy response — as of #52718 it returns a WP_Error when the check can't be performed, so true === … 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 to refresh_user_token() → 'authorize', minting a fresh user token without needing the old one. Everything below the branch is unchanged.

Behavior matrix

State Before After
User token missing locally, blog token healthy full reconnect (all users disconnected) 'authorize' (caller re-links; blog token and other users untouched)
User token missing, blog token broken/unreachable full reconnect full reconnect (unchanged)
Blog token missing locally full reconnect full reconnect (unchanged — validate_blog_token() can't sign, returns non-true)
Transient failure on validate() full reconnect full reconnect, unless the retried blog check confirms health → 'authorize' (strictly less destructive)
Both flags known-equal (both healthy / both broken) full reconnect unchanged
Unequal known flags / site connection surgical paths unchanged

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/blog already 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-existing invalid_connection_owner error stayed stored, so the broken-connection notice lingered until a second reconnect. That is a plain single-node bug, not a race:

  • Error_Handler clears stored errors by listening on jetpack_updated_user_token (→ delete_all_errors()).
  • Every user-token write funnels through Tokens::update_user_token(), but only the REST user-token endpoint fired jetpack_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.
  • The second click worked only because the first reconnect had by then given the user a token, so the subsequent disconnect_user() finally succeeded and fired jetpack_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 into Tokens::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() returns void, 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 to pre_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

Does this pull request change what data or activity we track or use?

No.

Testing instructions

Unit tests — jp test php packages/connection

  • Fix 1 — a new restore() suite in ManagerTest (partial-mocked Manager, spying on reconnect()) covers the full matrix above:
    • inconclusive validate() + confirmed-healthy blog token ⇒ reconnect() never called, returns 'authorize';
    • inconclusive + broken blog token ⇒ reconnect() called (unchanged);
    • inconclusive + unverifiable blog check (WP_Error) ⇒ reconnect() called (not treated as healthy);
    • all known-flag combinations (both healthy, both broken, blog-only, user-only) and the site-connection early branch route exactly as before.
  • Fix 2:
    • TokensTest::test_update_user_token_fires_action — a user-token write fires jetpack_updated_user_token with ($user_id, $token).
    • Error_Handler_Test::test_user_token_write_clears_stored_owner_error — the end-to-end guarantee: a stored invalid_connection_owner error is gone after a user-token write, via the production jetpack_updated_user_token → delete_all_errors() wiring.

Manual (a Jetpack-connected self-hosted site, e.g. Jurassic Ninja)

  1. Connect the site as an owner, and connect at least one secondary user (an admin or editor) to WordPress.com.
  2. Delete the owner's user token locally so the site is in the invalid_connection_owner / has_user_token: false state (e.g. clear the owner's row from the user_tokens option), leaving the blog token intact.
  3. As the owner, visit My Jetpack and click Restore Connection.
    • Before: the secondary user is disconnected too (they must re-authorize), and the broken-connection notice is still showing after you reconnect — it only clears on a second Restore Connection.
    • After: you're sent through authorization to re-link only your own account (the blog token and the secondary user's connection are untouched), and the notice is gone as soon as the reconnect completes — no second attempt.
  4. Sanity-check the unchanged paths: break only the blog token → it refreshes the blog token; break only your user token → it re-links your account; a genuinely broken connection (both) → full reconnect, as before.

Cost note: one extra signed GET (jetpack-token-health/blog), only in the previously-inconclusive branch of a user-initiated restore.

@fgiannar fgiannar self-assigned this Sep 28, 2026
@fgiannar
fgiannar requested review from a team as code owners September 28, 2026 05:32
@fgiannar fgiannar added Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] In Progress [Package] Connection labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the fix/restore-inconclusive-validation-blog-token branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/restore-inconclusive-validation-blog-token
bin/jetpack-downloader test jetpack-mu-wpcom-plugin fix/restore-inconclusive-validation-blog-token

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions Bot added [Plugin] Backup A plugin that allows users to save every change and get back online quickly with one-click restores. [Plugin] Boost A feature to speed up the site and improve performance. [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] Protect A plugin with features to protect a site: brute force protection, security scanning, and a WAF. [Plugin] Search A plugin to add an instant search modal to your site to help visitors find content faster. [Plugin] Social Issues about the Jetpack Social plugin [Plugin] Stats Data [Plugin] VideoPress A standalone plugin to add high-quality VideoPress videos to your site. [Tests] Includes Tests labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

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:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

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:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: October 6, 2026

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:

  • Next scheduled release: September 30, 2026

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.

@jp-launch-control

jp-launch-control Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 2 files.

File Coverage Δ% Δ Uncovered
projects/packages/connection/src/class-rest-connector.php 586/631 (92.87%) -0.01% 0 💚
projects/packages/connection/src/class-tokens.php 226/265 (85.28%) 0.11% 0 💚

Full summary · PHP report · JS report

@fgiannar fgiannar added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Sep 28, 2026
@fgiannar
fgiannar requested review from coder-karen and a balanced review from Copilot and removed request for a team September 28, 2026 10:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
coder-karen previously approved these changes Sep 28, 2026

@coder-karen coder-karen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, this tests well for me. I left a few nit comments, no blockers.

Comment thread projects/packages/connection/src/class-manager.php Outdated
Comment thread projects/packages/connection/changelog/fix-restore-inconclusive-validation Outdated
fgiannar and others added 4 commits September 29, 2026 07:46
…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
@fgiannar
fgiannar force-pushed the fix/restore-inconclusive-validation-blog-token branch from c0fa085 to e498016 Compare September 29, 2026 04:48
…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
@fgiannar
fgiannar merged commit 0e544df into trunk Sep 29, 2026
121 checks passed
@fgiannar
fgiannar deleted the fix/restore-inconclusive-validation-blog-token branch September 29, 2026 05:31
@github-actions github-actions Bot added [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. and removed [Status] Needs Review This PR is ready for review. labels Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Package] Connection [Plugin] Backup A plugin that allows users to save every change and get back online quickly with one-click restores. [Plugin] Boost A feature to speed up the site and improve performance. [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] Protect A plugin with features to protect a site: brute force protection, security scanning, and a WAF. [Plugin] Search A plugin to add an instant search modal to your site to help visitors find content faster. [Plugin] Social Issues about the Jetpack Social plugin [Plugin] Stats Data [Plugin] VideoPress A standalone plugin to add high-quality VideoPress videos to your site. [Status] UI Changes Add this to PRs that change the UI so documentation can be updated. [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants