phacility/phabricator · error · Exception

Cart is not charging yet!

Error message

Cart is not charging yet!

What it means

Exception thrown by PhortunePayPalPaymentProvider::processControllerRequest() when the 'charge' or 'cancel' action arrives but loadActiveCharge($cart) finds no active charge. These actions are PayPal's return/cancel callbacks: they only make sense in the middle of an initiated checkout that created a charge. Arriving without one means the callback is being replayed or hit directly after the charge already resolved (applied, held, or failed) and was consumed.

Solutions

  1. Send the user to the cart's checkout URI (cart->getCheckoutURI()) — the flow there already handles resolved carts gracefully with redirects instead of exceptions.
  2. In custom controllers wrapping these actions, pre-check for an active charge and issue a redirect response for the no-charge case rather than letting the provider throw.
  3. Accept idempotent callbacks: treat a second 'charge' hit on a purchased cart as a no-op redirect.

Example fix

// before
// GET /phortune/paypal/charge/?cartID=42&token=... a second time
// -> Exception('Cart is not charging yet!')

// after: guard the callback before dispatching to the provider
$charge = $controller->loadActiveCharge($cart);
if (!$charge) {
  return id(new AphrontRedirectResponse())
    ->setURI($cart->getCheckoutURI());
}
$response = $provider->processControllerRequest($controller, $request);
Defensive patterns

Strategy: validation

Validate before calling

// Guard the PayPal return/cancel callbacks before dispatch.
$charge = $controller->loadActiveCharge($cart);
$action = $controller->getAction();
if ($action === 'charge' || $action === 'cancel') {
  if (!$charge) {
    // Replay/direct hit after the charge already resolved: redirect gracefully.
    return id(new AphrontRedirectResponse())
      ->setURI($cart->getCheckoutURI());
  }
}
$response = $provider->processControllerRequest($controller, $request);

Try / catch

try {
  $response = $provider->processControllerRequest($controller, $request);
} catch (Exception $ex) {
  if (strpos($ex->getMessage(), 'Cart is not charging') !== false) {
    // Idempotent replay of the return URL: show the cart, not an error.
    return id(new AphrontRedirectResponse())
      ->setURI($cart->getCheckoutURI());
  }
  throw $ex;
}

Prevention

When it happens

Trigger: PayPal redirecting the buyer back to the return URL twice (refresh of the 'charge' page); a user bookmarking or re-opening the charge/cancel URL after completion; the cart status no longer being PURCHASING with the prior charge already resolved via didApplyCharge/didFailCharge; direct scraping/manual requests to those endpoints.

Common situations: Buyer refreshing the PayPal return landing page; email/IM sharing of the return URL; crawlers following redirect chains; double-fired redirects from PayPal sandbox flakiness.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/94bf7cb3535b3b17. Report an issue: GitHub.

Appendix: source

Thrown at src/applications/phortune/provider/PhortunePayPalPaymentProvider.php:308

    $viewer = $request->getUser();

    $cart = $controller->loadCart($request->getInt('cartID'));
    if (!$cart) {
      return new Aphront404Response();
    }

    $charge = $controller->loadActiveCharge($cart);
    switch ($controller->getAction()) {
      case 'checkout':
        if ($charge) {
          throw new Exception(pht('Cart is already charging!'));
        }
        break;
      case 'charge':
      case 'cancel':
        if (!$charge) {
          throw new Exception(pht('Cart is not charging yet!'));
        }
        break;
    }

    switch ($controller->getAction()) {
      case 'checkout':
        $return_uri = $this->getControllerURI(
          'charge',
          array(
            'cartID' => $cart->getID(),
          ));

        $cancel_uri = $this->getControllerURI(
          'cancel',
          array(
            'cartID' => $cart->getID(),
          ));

View on GitHub (pinned to 5720a38cfe)