{"record":{"id":"f1fc4512e9524b6c","repo":"hcengineering/platform","slug":"missing-subscription-data-f1fc45","errorCode":null,"errorMessage":"Missing subscription data","messagePattern":"Missing subscription data","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"services/payment/pod-payment/src/providers/stripe/webhook.ts","lineNumber":116,"sourceCode":"  } catch (err) {\n    ctx.error('Failed to process Stripe webhook', { err })\n    res.status(500).json({ error: 'Internal server error' })\n  }\n}\n\n/**\n * Handle subscription.created/updated/deleted\n */\nasync function handleSubscriptionUpdated (\n  ctx: MeasureContext,\n  accountsUrl: string,\n  serviceToken: string,\n  event: Stripe.Event\n): Promise<void> {\n  const subscription = event.data.object as Stripe.Subscription\n  if (subscription == null) {\n    ctx.error('Missing subscription data', { event })\n    throw new Error('Missing subscription data')\n  }\n\n  const subscriptionData = transformStripeSubscriptionToData(ctx, subscription)\n\n  if (subscriptionData === null) {\n    ctx.warn('Ignoring subscription in irrelevant state', {\n      subscriptionId: subscription.id,\n      status: subscription.status\n    })\n    return\n  }\n\n  const accountClient = getAccountClient(accountsUrl, serviceToken)\n  await accountClient.upsertSubscription(subscriptionData)\n\n  ctx.info('Subscription upserted', { subscriptionId: subscription.id, status: subscriptionData.status })\n}\n","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/payment/pod-payment/src/providers/stripe/webhook.ts#L98-L134","documentation":"The Stripe webhook handler for customer.subscription.updated casts event.data.object to Stripe.Subscription and requires it to be non-null before processing. If the event payload has no subscription object (malformed/duplicate/foreign event, or API-version shape change), it logs the event and throws 'Missing subscription data', which makes Stripe retry the webhook delivery.","triggerScenarios":"Receiving a customer.subscription.updated webhook whose data.object is null/empty — e.g. a replayed or corrupted event, an event from an unexpected object type, or a test event fired manually from the Stripe dashboard without a subscription payload.","commonSituations":"Sending a test webhook from the Stripe CLI/dashboard with a hand-crafted payload; webhook endpoint registered for extra event types whose objects are not subscriptions; Stripe API version upgrade changing event payload shape; duplicates/replays with stripped payloads.","solutions":["Verify the webhook endpoint only subscribes to customer.subscription.* events","Log event.id and event.type (already done via ctx.error) and locate the event in the Stripe dashboard to inspect its payload","If the event is genuinely malformed, return 200 and skip instead of throwing so Stripe stops retrying a permanently broken event","Pin/align the Stripe SDK API version with the account's webhook API version"],"exampleFix":"// before\nconst subscription = event.data.object as Stripe.Subscription\nif (subscription == null) {\n  ctx.error('Missing subscription data', { event })\n  throw new Error('Missing subscription data')\n}\n// after\nconst subscription = event.data.object as Stripe.Subscription | undefined\nif (subscription == null) {\n  ctx.warn('Ignoring webhook event without subscription data', { eventId: event.id, type: event.type })\n  return // do not fail; prevents endless Stripe retries on malformed events\n}","handlingStrategy":"type-guard","validationCode":"// before processing any webhook event\nif (event.type !== 'customer.subscription.updated') return\nconst subscription = event.data.object as Stripe.Subscription | undefined\nif (subscription == null || typeof subscription.id !== 'string') return // skip, don't throw","typeGuard":"function hasSubscriptionObject(event: Stripe.Event): event is Stripe.Event & { data: { object: Stripe.Subscription } } {\n  const obj = (event.data?.object ?? null) as Stripe.Subscription | null\n  return obj != null && typeof obj.id === 'string' && typeof obj.status === 'string'\n}","tryCatchPattern":"try {\n  await handleStripeWebhook(ctx, /* deps */, event)\n} catch (err) {\n  if ((err as Error).message === 'Missing subscription data') {\n    // log and acknowledge (200) so Stripe stops retrying a permanently malformed event\n    ctx.warn('Skipping malformed subscription.updated event', { eventId: event.id })\n    return\n  }\n  throw err\n}","preventionTips":["Restrict the webhook endpoint registration to customer.subscription.* events","Return 200 for unprocessable events instead of throwing, to avoid endless Stripe retries","Pin the Stripe SDK API version and update webhook handlers when upgrading","Replay-test webhook handling with the Stripe CLI before deploying"],"tags":["stripe","webhook","validation","event-handling"],"backgroundTag":"webhook-payload-missing-data","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}