{"record":{"id":"643d8c5adfd1c04f","repo":"phacility/phabricator","slug":"stripe-charge-call-did-not-return-an-id","errorCode":null,"errorMessage":"Stripe charge call did not return an ID!","messagePattern":"Stripe charge call did not return an ID!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"critical","filePath":"src/applications/phortune/provider/PhortuneStripePaymentProvider.php","lineNumber":149,"sourceCode":"    PhortuneCharge $charge) {\n    $this->loadStripeAPILibraries();\n\n    $price = $charge->getAmountAsCurrency();\n\n    $secret_key = $this->getSecretKey();\n    $params = array(\n      'amount'      => $price->getValueInUSDCents(),\n      'currency'    => $price->getCurrency(),\n      'customer'    => $method->getMetadataValue('stripe.customerID'),\n      'description' => $charge->getPHID(),\n      'capture'     => true,\n    );\n\n    $stripe_charge = Stripe_Charge::create($params, $secret_key);\n\n    $id = $stripe_charge->id;\n    if (!$id) {\n      throw new Exception(pht('Stripe charge call did not return an ID!'));\n    }\n\n    $charge->setMetadataValue('stripe.chargeID', $id);\n    $charge->save();\n  }\n\n  protected function executeRefund(\n    PhortuneCharge $charge,\n    PhortuneCharge $refund) {\n    $this->loadStripeAPILibraries();\n\n    $charge_id = $charge->getMetadataValue('stripe.chargeID');\n    if (!$charge_id) {\n      throw new Exception(\n        pht('Unable to refund charge; no Stripe chargeID!'));\n    }\n\n    $refund_cents = $refund","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/provider/PhortuneStripePaymentProvider.php#L131-L167","documentation":"Exception thrown by PhortuneStripePaymentProvider::executeCharge() after Stripe_Charge::create() returns an object whose ->id is falsy. The charge ID is the only durable reference Phortune stores (as 'stripe.chargeID' metadata) to later refund or update the charge, so losing it means a possibly-successful real charge exists at Stripe with no local handle — the worst state for a payment system. The check exists because the legacy Stripe SDK can return malformed/empty objects (or mocks can) without throwing.","triggerScenarios":"Stripe_Charge::create($params, $secret_key) succeeding at the HTTP level but the response lacking an id — old stripe-php library versions, unexpected API response shape from version drift between the bundled SDK and Stripe's API, or test doubles returning bare objects. Because the description param carries the Phortune charge PHID, an untracked charge can be found later in Stripe's dashboard by that description.","commonSituations":"Long-lived Phabricator installs with an outdated vendored Stripe library after Stripe deprecated API versions; sandbox mocking in tests that returns stdClass instead of Stripe_Charge with an id; proxy/charset corruption of the JSON response.","solutions":["Immediately reconcile: search the Stripe dashboard (or API) for a charge whose description equals the Phortune charge PHID before deciding whether money moved; never blind-retry the create call.","Upgrade the vendored Stripe PHP library to a version matching your Stripe API version and retest in sandbox mode.","If the charge truly was not created (dashboard shows nothing), retry the charge flow from the start with a fresh Phortune charge.","If it was created, record its ID onto the charge metadata manually, then let the normal apply-charge path continue."],"exampleFix":"// before\n$stripe_charge = Stripe_Charge::create($params, $secret_key);\n$id = $stripe_charge->id;\nif (!$id) {\n  throw new Exception(pht('Stripe charge call did not return an ID!'));\n}\n\n// after: verify, reconcile, and only then record\n$stripe_charge = Stripe_Charge::create($params, $secret_key);\n$id = idx($stripe_charge instanceof Stripe_Object ? $stripe_charge->__toArray() : array(), 'id');\nif (!$id) {\n  // Charge state unknown: look it up by description (charge PHID) before retrying.\n  phlog(pht('Stripe charge for %s returned no ID; manual reconciliation required.', $charge->getPHID()));\n  throw new Exception(\n    pht('Stripe charge call did not return an ID for %s; reconcile in Stripe.',\n      $charge->getPHID()));\n}\n$charge->setMetadataValue('stripe.chargeID', $id);\n$charge->save();","handlingStrategy":"try-catch","validationCode":"// Shape-check the SDK result before relying on it (mirrors the provider's\n// own guard, but lets you branch instead of catching).\nfunction stripe_charge_has_id($stripe_charge) {\n  return is_object($stripe_charge) && !empty($stripe_charge->id);\n}\n\n$stripe_charge = Stripe_Charge::create($params, $secret_key);\nif (!stripe_charge_has_id($stripe_charge)) {\n  // Unknown money state: reconcile by description (charge PHID) in the\n  // Stripe dashboard before any retry. DO NOT blind-retry the create.","typeGuard":"function stripe_charge_has_id($stripe_charge) {\n  return is_object($stripe_charge) && !empty($stripe_charge->id);\n}","tryCatchPattern":"try {\n  $provider->executeCharge($method, $charge);\n} catch (Exception $ex) {\n  if (strpos($ex->getMessage(), 'did not return an ID') !== false) {\n    // Charge state unknown: the Phortune charge PHID was sent as the Stripe\n    // 'description', so a real charge (if any) is findable by it.\n    phlog(pht(\n      'UNRESOLVED STRIPE CHARGE %s: reconcile by description before retry.',\n      $charge->getPHID()));\n  }\n  throw $ex;\n}","preventionTips":["Pin the vendored stripe-php version to one matching your Stripe API version; retest in sandbox after API upgrades.","Never auto-retry a create that failed this way — a live charge may already exist; reconcile by the charge PHID in the description first.","Make test doubles return realistic Stripe_Charge objects with ids so this guard never fires in CI.","Monitor for this exception with high priority: it means untracked money may have moved."],"tags":["php","phabricator","phortune","stripe","payment-integrity","api-response","payments"],"backgroundTag":"payment-gateway-invalid-response","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}