Preface/Warning

We had this issue on a site that's using a patched version of commerce_stripe and Opus 4.7 was used to debug the issue. The issue and proposed changes makes sense to to me, but I haven't followed the commerce core changes recently enough to know if this is correct.
The gist is that payment plugins throw exceptions, but the upstream code doesn't seem to catch them. If that's intended, just close this out without reading :D

Problem/Motivation

Commerce 3.3.0 introduced the new unified merchant-facing payment form, \Drupal\commerce_payment\Form\OrderPaymentAddForm, and an OrderPaymentAddFormRouteSubscriber that swaps the entity.commerce_payment.add_form route to use it (unless commerce_payment_use_legacy_add_payment_form is set). This form replaces the legacy PaymentAddForm.

Per the payment gateway contract, PaymentGatewayInterface::createPayment() throws a DeclineException / HardDeclineException (subclasses of PaymentGatewayException) when a charge is declined. Every other payment entry point in core wraps the call and reacts to it:

  • Plugin/Commerce/CheckoutPane/PaymentProcess
  • PluginForm/PaymentMethodAddForm and PaymentMethodEditForm (via the inline PaymentGatewayForm)
  • Controller/PaymentCheckoutController
  • the legacy PaymentAddForm, which charges through buildInlineForm()PaymentGatewayForm (which catches DeclineException)

OrderPaymentAddForm::submitForm() calls $payment_gateway_plugin->createPayment($payment, $capture) directly, with no try/catch. When a real gateway (Stripe, PayPal, Authorize.Net, etc.) declines a card, the thrown exception is uncaught and the merchant gets a generic "The website encountered an unexpected error" WSOD instead of the decline reason — and no payment is recorded. Card-on-file / MOTO charges from the admin order screen are the common trigger.

$ grep -c catch modules/payment/src/Form/OrderPaymentAddForm.php
0

Steps to reproduce

  1. Use Commerce 3.3.x with the new payment form active (default).
  2. Configure an on-site gateway that can decline (e.g. Commerce Stripe).
  3. As a merchant, go to /admin/commerce/orders/{order}/payments/add.
  4. Enter a card the gateway will decline (e.g. a Stripe test card that triggers card_declined).
  5. Submit.

Expected: the form rebuilds and shows the decline message (as checkout and the legacy form do).

Actual: uncaught DeclineException → WSOD; nothing is saved.

Proposed resolution

Wrap the createPayment() calls in OrderPaymentAddForm::submitForm() in a try/catch, mirroring PaymentGatewayForm / PaymentProcess: catch DeclineException (show the message, rebuild) and PaymentGatewayException (log + generic message, rebuild), instead of letting them bubble up. Catch order matters — most-specific first, so DeclineException (and its Hard/Soft subclasses) is handled before the PaymentGatewayException catch-all.

use Drupal\commerce_payment\Exception\DeclineException;
use Drupal\commerce_payment\Exception\PaymentGatewayException;

// In submitForm(), step 4 ("Process the payment using the gateway plugin."):
try {
  if ($payment_gateway_plugin instanceof ManualPaymentGatewayInterface) {
    $received = (bool) $form_state->getValue('payment_received');
    $payment_gateway_plugin->createPayment($payment, $received);
  }
  else {
    $capture = ($form_state->getValue('transaction_type') === 'capture');
    $payment_gateway_plugin->createPayment($payment, $capture);
  }
}
catch (DeclineException $e) {
  $this->messenger()->addError($e->getMessage());
  $form_state->setRebuild();
  return;
}
catch (PaymentGatewayException $e) {
  $this->logger('commerce_payment')->error($e->getMessage());
  $this->messenger()->addError($this->t('We encountered an error processing your payment. Please try again or use a different payment method.'));
  $form_state->setRebuild();
  return;
}
// Steps 5 & 6 (save references, confirmation, redirect) only run on success.

Remaining tasks

  • Patch OrderPaymentAddForm::submitForm() to catch decline / gateway exceptions.
  • Add test coverage for a declining gateway on the merchant payment-add form.

User interface changes

On a declined payment, the merchant now sees the decline message on a rebuilt form instead of a WSOD. No other UI changes.

API changes

None.

Data model changes

None.

Release notes snippet

Fixed an uncaught exception (WSOD) when a payment is declined on the new merchant-facing "Add payment" form; the decline reason is now shown and the form is rebuilt.

Issue fork commerce-3593063

Command icon 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

andyg5000 created an issue. See original summary.

andyg5000’s picture

Issue summary: View changes
andyg5000’s picture

Issue summary: View changes

andyg5000’s picture

Status: Active » Needs review
andyg5000’s picture

Assigned: andyg5000 » Unassigned

jsacksick made their first commit to this issue’s fork.

jsacksick’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.