Skip to content

fix: close race where updateIntent resolved before iframes applied new sdkAuthorization - #1738

Open
aritro2002 wants to merge 4 commits into
mainfrom
cvc-widget-race-condition
Open

fix: close race where updateIntent resolved before iframes applied new sdkAuthorization#1738
aritro2002 wants to merge 4 commits into
mainfrom
cvc-widget-race-condition

Conversation

@aritro2002

Copy link
Copy Markdown
Contributor

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

updateIntent() could resolve before the widgets it just updated (CardCVCElement, ExpressCheckoutElement, etc.) had actually applied the new sdkAuthorization — so a confirm fired right after updateIntent could go out with the old session and get a 401 from the backend ("API key not provided or invalid API key used").

Root cause

  • paymentSession.updateIntent hardcoded shouldWaitForReady=false
    it never waited for any iframe acknowledgment before resolving.
  • elements.updateIntent only waited if a PaymentElement happened to
    be mounted (paymentElementIframeRef->Array.length > 0); any other
    widget (a standalone CVC widget, headless, express checkout) got no
    wait at all.
  • The credential update itself travels to each content iframe via
    postMessage("ElementsUpdate", ...), which is fire-and-forget — the
    receiving iframe applies it to its own local state asynchronously.

So await updateIntent(...) could return while a mounted widget was still holding the old credentials, and a confirm triggered right after would race it.

Fix

  • UpdateIntentHelpersNew.res: added waitForElementsUpdateAcks,
    which counts the mounted iframes and blocks performUpdateIntent
    from resolving until it's received one {elementsUpdateApplied: true}
    ack per iframe (bounded by a 5s timeout so a non-responsive/torn-down
    iframe can't hang it forever). Runs unconditionally for every
    updateIntent call, regardless of entry point or widget mix.
  • LoaderController.res: each content iframe now sends that ack from a
    React.useEffect keyed on keys.sdkAuthorization, not synchronously
    inside the "ElementsUpdate" message handler. setKeys/setConfig
    are React state setters — they don't apply immediately, so an ack sent
    right after calling them doesn't actually guarantee the new value is
    live yet. The effect only fires once React has committed the change,
    which is the actual guarantee callers need. A ref skips the ack on
    initial mount so it only fires on real updates.

How did you test it?

Reproduced the race with a dedicated test button (paymentSession.updateIntent awaited, followed immediately by
confirmWithLastUsedPaymentMethod, zero delay) — reliably got 401 before this fix. After the fix, ran the same sequence 5x back-to-back with fresh page loads each time: /confirm returned 200 every time.

before:
https://github.com/user-attachments/assets/296635c5-96a2-421c-bb59-fc55485c8cfe

after:
https://github.com/user-attachments/assets/b49bb292-9ec9-4003-9923-95bfbeb00a03

Checklist

  • I ran npm run re:build
  • I reviewed submitted code
  • I added unit tests for my changes where possible

@aritro2002 aritro2002 self-assigned this Aug 31, 2026
@aritro2002 aritro2002 added the Ready for Review PR with label Ready for Review should only be reviewed. label Aug 31, 2026
@semanticdiff-com

semanticdiff-com Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review changes with  SemanticDiff

@XyneSpaces

Copy link
Copy Markdown

Automated review findings (posted as PR-level comment because inline review-comment creation is unavailable in this run):

  1. src/hyper-loader/UpdateIntentHelpersNew.res (inside waitForElementsUpdateAcks)
    ⚠️ The ack counter increments on any incoming message containing elementsUpdateApplied: true, without validating event.origin or correlating the message to one of the updated iframes. A hostile or misconfigured frame could spoof the ack and cause updateIntent to resolve while some widgets still hold stale credentials.

  2. src/LoaderController.res (the elementsUpdateApplied ack useEffect)
    Possible issue — worth verifying: The ack effect is keyed off keys.sdkAuthorization. If performUpdateIntent renews only clientSecret while sdkAuthorization remains unchanged, the iframe never emits elementsUpdateApplied, so the parent times out and may proceed before the widget has live credentials.

Comment thread webpack.dev.js Outdated
headers: {
"Cache-Control": "must-revalidate",
},
allowedHosts: "all",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the reason for this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had made this changes for cypress, so, basically ->
The application was running on localhost:9060 and was trying to load HyperLoader.js from localhost:9050, which was being blocked due to the different origins.

To fix this, I added allowedHosts: "all".

I will remove this changes, create a separate PR for it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for Review PR with label Ready for Review should only be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: close race where updateIntent resolved before iframes applied new sdkAuthorization

3 participants