Description
When canceling an offsite payment (e.g., PayPal, 2C2P), the PaymentCheckoutController::cancelPage() method retrieves the order entity from the route parameters. This order may be stale, especially if a previous cancel or payment notification request has already updated the order.
This issue becomes more likely when:
- Multiple cancel requests are triggered within a short time (e.g., user clicking "Back" multiple times)
The controller then passes this potentially outdated $order into:
$payment_gateway_plugin->onCancel($order, $request)
$checkout_flow_plugin->redirectToStep($previous_step_id)
Although cancelPage() itself does not modify or save the order, these plugin methods often do. If the $order object is stale, saving it will trigger the following exception:
Drupal\commerce_order\Exception\OrderVersionMismatchException: Attempted to save order 1234 with version 8. Current version is 10
Steps To Reproduce
- Configure an offsite payment gateway that supports both redirect and server-side notification (e.g., 2C2P, PayPal).
- Begin the checkout process and proceed to the payment gateway.
- Cancel the payment in a way that triggers multiple browser-based cancel requests (e.g., double-clicking back or refreshing).
- Observe that one request updates the order successfully.
- Subsequent requests fail with
OrderVersionMismatchExceptionwhen plugins attempt to save the outdated order.
Proposed Solution
Reload the order using loadForUpdate() inside cancelPage(), before passing it to plugins. Also, update the route parameters with the reloaded order, since the checkout flow and payment plugins rely on the route-matched version.
Issue fork commerce-3539545
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #6
mohammad-fayoumiComment #7
jsacksick commentedOk so 8.x-2.x is no longer maintained, we just need an MR for 3.x.
Additionally, the checkout flow plugin now has a
setOrder()method that we could use here without the need to interfere with the route match I believe, unless some other code is relying on it?So we could update the code to look like the following:
Technically the payment plugins shouldn't rely on the route match parameters from the onCancel() method as the order is passed there?
Comment #8
jsacksick commentedI did implement the change proposed, See https://git.drupalcode.org/project/commerce/-/merge_requests/485/diffs?c....
Since we're making this change in
cancelPage(), I wonder if we should do the same fromreturnPage()(i.e. set the order on the checkout flow plugin rather than messing with the route match parameters but a bit more hesitant to change this).Comment #9
jsacksick commentedThe change from the commit linked in #8 introduced PHPSTAN issues. Slightly adapted the change, See: https://git.drupalcode.org/project/commerce/-/merge_requests/485/diffs?c...
Comment #10
jsacksick commentedI actually decided to rewrite
returnPage()as well for consistency.Plugins using the route match rather than the passed order should be rewritten. But I don't believe this is the case, I think the OrderVersionMismatchException was thrown due to the checkout step redirection login from the checkout flow plugin which was always using the order from the route match. Using the setter, the order is set afresh after loading it via loadForUpdate().
Comment #12
jsacksick commented