Skip to content

"Simulate webhooks" has been non-functional since 3.3.1 (reports false failure on every site, and false success on any site that ran it before 3.3.1) #4627

Description

@namiokuzono

TL;DR: The Troubleshooting screen's "Simulate webhooks" button has done nothing since 3.3.1. WebhookSimulation::start() returns immediately, so the test never runs. Merchants are told after 60 seconds that their site is unreachable from the internet — or, if the site ran a simulation before 3.3.1, are told instantly that it succeeded.

What happens

The button is still rendered and the documentation still tells merchants to use it as the way to verify webhook delivery, but it no longer contacts PayPal. Both of its outcomes are meaningless, and both actively mislead:

  • False failure, on every site. After 30 polls over 60 seconds the merchant sees "Looks like the webhook cannot be received. Check that your website is accessible from the internet." This sends people to their host to investigate a network problem that does not exist.
  • False success, on any site that ran a successful simulation before 3.3.1. The state is read from an option that survives upgrades, so a stale received value returns instantly and indefinitely, without a request leaving the site.

The second is the more damaging: a green result is taken as proof that webhook delivery is healthy, which closes off the investigation on stores where it is genuinely broken.

How this surfaced

Found while working two unrelated support cases, a few weeks apart. Both merchants had a real problem, and in both the button pointed the investigation away from it.

In the first, a card was charged but the order never completed. The merchant ran the simulation, got "the webhook cannot be received", and reasonably concluded their host was blocking PayPal. It was not — the site's webhook endpoint was reachable from outside and responding correctly. Support had to tell them to disregard the message entirely and stop pursuing the hosting angle.

In the second, refunds issued from the PayPal dashboard were not updating the WooCommerce order. The simulation returned "The webhook was received successfully", which was taken as evidence that webhook delivery was healthy, so the investigation moved on to the refund handler. Delivery was in fact completely dead on that store — no PayPal event had ever reached it. That misdirection cost roughly three weeks across two tickets and an escalation before the contradiction was spotted.

The current support guidance on both cases ended up being the same: ignore whatever the button says, and check the Notification URL and the extension log instead.

Technical context (verified against trunk @ 36f0f0f78, release 4.1.2)

The cause is #3908 (commit cc4b23aab, 2025-12-04, merged to dev/release/3.3.1). Its stated purpose was narrower than what it did: "disables calling simulate webhook and resubscribe webhooks on plugin upgrade." It added return; in two places:

File Location Effect
modules/ppcp-webhooks/src/WebhookModule.php inside the woocommerce_paypal_payments_gateway_migrate handler Achieves the stated goal — this is the upgrade path
modules/ppcp-webhooks/src/Status/WebhookSimulation.php :66-67 Kills the shared method, which the user-facing button also depends on

The second guard is not needed for the stated goal, and it is what broke the feature. Everything below it is unreachable and marked @phpstan-ignore deadCode.unreachable.

The failure path:

Step File Line What happens
1 modules/ppcp-settings/resources/js/.../Blocks/SimulationBlock.js :110 Button calls startSimulation(30)
2 modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php :167-169 simulate_webhooks_start() calls WebhookSimulation::start(), returns success
3 modules/ppcp-webhooks/src/Status/WebhookSimulation.php :65-67 // Disabled for 3.3.1 release. → unconditional return;. No request to PayPal, no option written
4 .../Blocks/SimulationBlock.js :59-60 JS polls 30× at 2s intervals
5 .../Endpoint/WebhookSettingsEndpoint.php.../WebhookSimulation.php :182-192, :128, :164-167 check_simulated_webhook_state()get_state()load()get_option( 'ppcp-webhook-simulation' ) is empty → UnexpectedValueException, caught and returned as success: false on every poll
6 .../Blocks/SimulationBlock.js :89-94 After 60s: "Looks like the webhook cannot be received…"

And the false-success path: save() at :154-156 is the only writer of ppcp-webhook-simulation, reachable from the now-dead start() (:80) and from receive() (:112-119, live events only). The only code that removes it is the uninstall data-clear routine, which lists it in modules/ppcp-uninstall/services.php:33nothing removes it during normal operation, and nothing removes it on upgrade. A site that ran a successful simulation before 3.3.1 therefore still holds state => received after updating, so step 5 returns received on the first poll and step 4's loop short-circuits to the success notice at :72-79.

WebhookRegistrar::do_register():102 clears ppcp-last-webhook-event, which is a different option. Re-registering does not reset the simulation state.

The decision this needs, rather than a one-line revert: WebhookSimulation::start() is also called from WebhookRegistrar::do_register():105, and do_register() runs from WebhookModule's admin_init auto-recovery on connected installs (throttled to five minutes). Simply deleting the return; would fire a simulate event to PayPal on every automatic re-registration, which may be what the original change was avoiding. Guarding at the do_register() call site instead of inside start() would keep automatic registration quiet while leaving the merchant-facing button working.

Steps to reproduce

PayPal Payments: 3.3.1 through 4.1.2 (any)
WooCommerce / WordPress / PHP: not a factor — the path is options-only
Checkout: N/A — admin settings screen only
HPOS: N/A — no order data on this path
Other active plugins: none required

A — false failure (any site, any host):

  1. Connect the extension to a PayPal account.
  2. Go to Payments → PayPal → Settings → Troubleshooting.
  3. Click Simulate webhooks.
  4. Wait 60 seconds.
  5. Observe the notice at the top of the screen.
  • Expected: either a success notice, or a failure that reflects a real delivery problem.
  • Actual: "Looks like the webhook cannot be received. Check that your website is accessible from the internet." — on every site, including one whose webhook endpoint is publicly reachable and returning 401 rest_forbidden to an unsigned POST (the healthy response).

B — false success (site that ran a successful simulation before 3.3.1):

  1. On a site running PayPal Payments < 3.3.1, click Simulate webhooks and let it succeed.
  2. Update the extension to 3.3.1 or later.
  3. Go to Payments → PayPal → Settings → Troubleshooting and click Simulate webhooks.
  • Expected: a fresh test, or no result at all.
  • Actual: "The webhook was received successfully." within about two seconds, with no outbound request.

Step 1 of B is the part that has not been executed from a clean install — the pre-3.3.1 starting state was inferred from the code rather than recreated. Everything in the Actual lines has been observed on live stores running 4.1.1.

Evidence

Observed on a store running 4.1.1, over a single day:

Sequence Observation Source
Morning System status report reads Webhook status: – Three SSR exports, byte-identical
~2 hours later "✓ The webhook was received successfully." notice on the Troubleshooting screen Screenshot of the admin screen
~50 minutes after that Extension log, a 19-minute window containing two successful PayPal captures. No received of type line (IncomingWebhookEndpoint.php:253-259), and no ERROR line from any verify_request() rejection (:164, :178, :185, :201, :206). Every line in the file is DEBUG PPCP day-log

The success notice cannot have come from a delivered event, because on 4.1.1 start() returns before any request is made — no simulated event was ever asked for. The log later that day independently shows that nothing was reaching PHP at all: no event was accepted and none was rejected, across a window that included two live captures which PayPal notifies on within seconds.

Axes checked

Axis Verdict
Version delta Present in every release from 3.3.1 (2025-12-04) to 4.1.2 (2026-08-04), and still present on dev/develop at 04c5349ed (2026-08-21)
Recent regression Yes — cc4b23aab. Before it, the button worked
HPOS Ruled out — options-only path, no order data
Block vs classic Ruled out — admin settings screen
Server vs client Both: the PHP early-return and the JS retry loop. Fully provable from code
Settings & filters No setting, filter or feature flag re-enables it — the return; is unconditional
Third-party interference Ruled out — the failure is internal and reproduces with no other plugins active
Host / environment Ruled out as a variable — the outcome depends only on whether a pre-3.3.1 simulation ever succeeded on that site
Related feature still working "Resubscribe webhooks" is unaffected (WebhookSettingsEndpoint::resubscribe_webhooks()WebhookRegistrar::register()). Only the upgrade-triggered re-registration was disabled by the same commit

Not verified

  • Whether the original intent was specifically to stop simulate events firing on automatic re-registration, or only on upgrade. WebhookRegistrar.php:105 suggests the former, but the PR description says only "on plugin upgrade".
  • Whether any site holds ppcp-webhook-simulation in STATE_WAITING rather than received, and what that produces. Not tested.
  • Step B1 has not been executed from a clean pre-3.3.1 install, as noted above.
  • How many merchants are affected. The two cases above are the ones that reached support and were diagnosed; a merchant who accepts the "your site is unreachable" message and goes to their host does not generate a ticket at all, so the real number is likely higher.

Impact

Affects every merchant on 3.3.1 or later who uses the Troubleshooting screen — which the documentation directs them to. The button is the only self-service webhook check the extension offers, and it is also the first check support asks for.

Workaround: none for the button. Ignore both of its outcomes. The checks that do work are the Notification URL on the same screen (confirm it matches the site's own domain), the Webhook status row in the system status report, and the extension log — which records a line for every webhook that arrives and an error for every one it rejects, so a silent log means nothing is reaching the site.

Related

  • Disable simulate-event endpoint for webhook verification (5621) #3908 — the change that introduced this. Not a duplicate; it is the cause, and its stated scope is narrower than its effect.
  • ERROR Failed to retrieve stored webhook data. #1118 — the same Failed to retrieve stored webhook data exception surfacing, from 2023. Pre-dates this by two years and has a different cause; noted so it is not mistaken for a duplicate.
  • The documentation still instructs merchants to use this button as the verification step, and the in-product "Webhook Status documentation" link (Troubleshooting.js:46) points at an anchor that no longer exists on the target page. Both want fixing alongside, or removing the button will leave the docs stranded.

Suggested direction

Either restore the simulation for the merchant-facing path — most likely by moving the guard from WebhookSimulation::start() to the WebhookRegistrar::do_register():105 call site, so automatic re-registration stays quiet — or remove <SimulationBlock /> from Troubleshooting.js:51 and update the documentation. Leaving a rendered button that cannot work is the worst of the three.

Whichever way it goes, ppcp-webhook-simulation should be cleared on upgrade, so stale received values stop producing false passes.


Investigated and written with Claude Code / Opus.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions