In a recent merchant case 11563873-zen, the complain was that intermittently add to cart does not work - the item added to cart message displays fine - clicking the basket/cart link, it shows Cart is empty.
After session logging - it was seen that the ?wc-ajax=ppc-simulate-cart is potentially overwriting a stale session due to race condition between the POST add to cart request and the ?wc-ajax=ppc-simulate-cart which can occur right during the add to cart request.
One needs to be lucky to replicate this - however an intentional delay described below helps
- A simple product
- A shipping method that applies to all locations regardless anything (Eg flat rate)
- An intentional delay added to wc_ajax_ppc-simulate-cart'
add_action( 'wc_ajax_ppc-simulate-cart', 'ks_delay_forced_ppc_simulation', 1 );
function ks_delay_forced_ppc_simulation() {
$forced = isset( $_GET['ks_forced'] ) ? sanitize_text_field( wp_unslash( $_GET['ks_forced'] ) ) : '';
if ( 'timeout' === $forced ) {
usleep( 2000000 );
}
}
- Force the PPC simulate cart request via JS
(() => {
const delayMs = 300;
const form = document.querySelector('form.cart');
const button = form.querySelector('.single_add_to_cart_button');
const config = window.PayPalCommerceGateway;
button.addEventListener('click', () => {
const payload = {
nonce: config.ajax.simulate_cart.nonce,
products: [{
id: form.querySelector('[name="add-to-cart"]').value,
quantity: form.querySelector('[name="quantity"]')?.value || 1,
variations: [...form.querySelectorAll('[name^="attribute_"]')]
.map(({ name, value }) => ({ name, value }))
}]
};
setTimeout(() => {
const endpoint = new URL(
config.ajax.simulate_cart.endpoint,
location.href
);
endpoint.searchParams.set('ks_forced', 'timeout');
fetch(endpoint, {
method: 'POST',
credentials: 'same-origin',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}, delayMs);
}, { capture: true });
})();
- Run the above JS before pressing add to cart on the frontend
- See Cart is empty
In practical scenarios - the PPC request can naturally race with the POST add to cart - eg qty change -> Add to cart or variation change -> Add to cart.
What happens when the requests race
ppc-simulate-cart initializes WooCommerce using an existing cartless/stale session snapshot.
IsolatedCartSimulator creates a temporary cart and calls $cart->calculate_totals().
- Shipping calculation uses the global
WC()->shipping() instance, which writes shipping_for_package_0 into the customer's real WC()->session.
- This marks the real session as dirty, despite the simulated cart being intended to be isolated.
- The concurrent Add-to-cart POST completes and saves a session containing the newly added
cart.
- The delayed simulation finishes afterward. During shutdown,
WC_Session_Handler::save_data() replaces the complete serialized session row using its older snapshot.
- Because this is a whole-row, last-writer-wins update without merging individual session keys, the simulation's cartless snapshot removes the
cart written by the Add-to-cart request.
The resulting write order is:
Add-to-cart POST:
session = { ..., cart, cart_totals, shipping_for_package_0 }
ppc-simulate-cart finishes afterward:
session = { ..., shipping_for_package_0 } // no cart
It was additionally observed that ppc-simulate-cart runs even when no PayPal button is visibly rendered on the product page. The product controller appears to gate simulation on the presence of form.cart and simulate_cart.enabled, rather than on whether a PayPal button was successfully rendered or is visible.
In a recent merchant case 11563873-zen, the complain was that intermittently add to cart does not work - the item added to cart message displays fine - clicking the basket/cart link, it shows Cart is empty.
After session logging - it was seen that the
?wc-ajax=ppc-simulate-cartis potentially overwriting a stale session due to race condition between the POST add to cart request and the?wc-ajax=ppc-simulate-cartwhich can occur right during the add to cart request.One needs to be lucky to replicate this - however an intentional delay described below helps
In practical scenarios - the PPC request can naturally race with the POST add to cart - eg qty change -> Add to cart or variation change -> Add to cart.
What happens when the requests race
ppc-simulate-cartinitializes WooCommerce using an existing cartless/stale session snapshot.IsolatedCartSimulatorcreates a temporary cart and calls$cart->calculate_totals().WC()->shipping()instance, which writesshipping_for_package_0into the customer's realWC()->session.cart.WC_Session_Handler::save_data()replaces the complete serialized session row using its older snapshot.cartwritten by the Add-to-cart request.The resulting write order is:
It was additionally observed that
ppc-simulate-cartruns even when no PayPal button is visibly rendered on the product page. The product controller appears to gate simulation on the presence ofform.cartandsimulate_cart.enabled, rather than on whether a PayPal button was successfully rendered or is visible.