phacility/phabricator · error · Exception

Unable to update charge; no Stripe chargeID!

Error message

Unable to update charge; no Stripe chargeID!

What it means

Exception thrown by PhortuneStripePaymentProvider::updateCharge() when asked to refresh a charge that has no 'stripe.chargeID' metadata. The update path is Stripe_Charge::retrieve($charge_id, ...) — with no stored ID there is nothing to retrieve. The metadata is only written at the end of a successful executeCharge(), so this indicates the charge never completed (or was not a Stripe charge).

Source

Thrown at src/applications/phortune/provider/PhortuneStripePaymentProvider.php:194

    $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
    $stripe_refund = $stripe_charge->refunds->create($params);

    $id = $stripe_refund->id;
    if (!$id) {
      throw new Exception(pht('Stripe refund call did not return an ID!'));
    }

    $charge->setMetadataValue('stripe.refundID', $id);
    $charge->save();
  }

  public function updateCharge(PhortuneCharge $charge) {
    $this->loadStripeAPILibraries();

    $charge_id = $charge->getMetadataValue('stripe.chargeID');
    if (!$charge_id) {
      throw new Exception(
        pht('Unable to update charge; no Stripe chargeID!'));
    }

    $secret_key = $this->getSecretKey();
    $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);

    // TODO: Deal with disputes / chargebacks / surprising refunds.

  }

  private function getPublishableKey() {
    return $this
      ->getProviderConfig()
      ->getMetadataValue(self::STRIPE_PUBLISHABLE_KEY);
  }

  private function getSecretKey() {
    return $this

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Skip charges without 'stripe.chargeID' — their original flow (or its failure) must finish first.
  2. Limit update polling to CHARGED/HOLD charges that carry Stripe metadata and belong to the Stripe provider.
  3. If a real Stripe charge exists (searchable by charge PHID in the description), restore the metadata before polling.

Example fix

// before
$provider->updateCharge($charge);

// after
$charge_id = $charge->getMetadataValue('stripe.chargeID');
if (!strlen((string)$charge_id)) {
  // No Stripe reference on this charge; nothing to update.
  return;
}
$provider->updateCharge($charge);
Defensive patterns

Strategy: validation

Validate before calling

// Only update charges that carry a Stripe reference.
function stripe_charge_is_updatable(PhortuneCharge $charge) {
  return strlen((string) $charge->getMetadataValue('stripe.chargeID')) > 0;
}

if (!stripe_charge_is_updatable($charge)) {
  // No Stripe object to retrieve; leave the charge to its own flow.
  continue;
}
$provider->updateCharge($charge);

Type guard

function charge_has_stripe_charge_id(PhortuneCharge $charge) {
  $id = $charge->getMetadataValue('stripe.chargeID');
  return is_string($id) && strlen($id) > 0;
}

Try / catch

try {
  $provider->updateCharge($charge);
} catch (Exception $ex) {
  // Missing chargeID means the charge never completed via Stripe:
  // log and exclude from future polling instead of retrying.
  phlog(pht('Cannot update charge %s: %s', $charge->getPHID(), $ex->getMessage()));
}

Prevention

When it happens

Trigger: A charge-status worker or admin tool calling updateCharge() on a charge in HOLD/FAIL status whose executeCharge() never reached the save; polling a charge created by a different provider through the Stripe provider; metadata missing after a crash between charge creation and persistence.

Common situations: Generic pollers that update every open charge regardless of provider; test fixtures with hand-built charges; scripts run against carts stuck mid-checkout after a Stripe outage.

Related errors


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