Skip to content

fix(widget): add iframe loading retry logic#7636

Open
Danziger wants to merge 12 commits into
developfrom
feat/fix-widget-retry-logic
Open

fix(widget): add iframe loading retry logic#7636
Danziger wants to merge 12 commits into
developfrom
feat/fix-widget-retry-logic

Conversation

@Danziger

@Danziger Danziger commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

The biggest problem was in the fact that we need to rebind all the listeners connected to the iframe window, because after retry the iframe.contentWindow is a new instance.

I didn't add a loading state to the error screen in this PR. I think we should add it separately as a generic solution and display it in case of the default iframe loading (not after retry).

tteee.mov

To Test

  1. Go to the widget configurator preview in this PR
  2. Block the iframe URL in dev tools. (set a valid CoW Swap URL from the preview first)
  3. Refresh the page
  • After 30 seconds, it should display the error state
  1. Click "Reload"
  • After 30 seconds, it should display the error state
  1. Unblock the iframe URL in dev tools
  2. Click "Reload"
  • After a while, it should load the widget content
  • The widget configurator should have an impact on the widget

Summary by CodeRabbit

  • New Features

    • Improved widget loading behavior with clearer recovery when the widget fails to load.
    • Added a loading error callback in the widget preview flow so the configurator can react to failures.
  • Bug Fixes

    • Retry actions from the error screen now work more reliably.
    • Loading state detection is more robust, reducing cases where the widget appears stuck or incorrectly marked as loaded.

@Danziger Danziger requested review from a team and shoom3301 June 9, 2026 14:47
@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Jun 23, 2026 2:31pm
explorer-dev Ready Ready Preview Jun 23, 2026 2:31pm
storybook Ready Ready Preview Jun 23, 2026 2:31pm
swap-dev Ready Ready Preview Jun 23, 2026 2:31pm
widget-configurator Ready Ready Preview Jun 23, 2026 2:31pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Jun 23, 2026 2:31pm
sdk-tools Ignored Ignored Preview Jun 23, 2026 2:31pm

Request Review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploying explorer-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: e988bb3
Status: ✅  Deploy successful!
Preview URL: https://271c1d6e.explorer-dev-dxz.pages.dev
Branch Preview URL: https://feat-fix-widget-retry-logic.explorer-dev-dxz.pages.dev

View logs

Comment thread libs/widget-lib/src/widgetIframeLoading.ts

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
libs/widget-lib/src/widgetIframeLoading.ts (1)

163-174: 💤 Low value

Consider removing the message listener in onWidgetReady.

The onRetryMessage listener (added on line 159) is only removed in cancelWidgetLoading, not in onWidgetReady. After the widget loads successfully, this listener remains attached and fires on every window message event, returning early due to the isLoaded check. While functionally harmless, it adds unnecessary overhead.

♻️ Suggested fix
     onWidgetReady() {
       isLoaded = true
       clearTimeout(loadingTimeoutID)
+      window.removeEventListener('message', onRetryMessage)
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/widget-lib/src/widgetIframeLoading.ts` around lines 163 - 174, The
onRetryMessage listener is only removed in cancelWidgetLoading, so after a
successful load the window 'message' listener remains attached; update the
onWidgetReady handler to also remove the listener (call
window.removeEventListener('message', onRetryMessage)) and perform the same
cleanup you do on cancel (e.g., clearTimeout(loadingTimeoutID) and optionally
call cleanUpLoadCheck()) so the listener and timers are cleared when the widget
loads successfully.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@libs/widget-lib/src/widgetIframeLoading.ts`:
- Around line 163-174: The onRetryMessage listener is only removed in
cancelWidgetLoading, so after a successful load the window 'message' listener
remains attached; update the onWidgetReady handler to also remove the listener
(call window.removeEventListener('message', onRetryMessage)) and perform the
same cleanup you do on cancel (e.g., clearTimeout(loadingTimeoutID) and
optionally call cleanUpLoadCheck()) so the listener and timers are cleared when
the widget loads successfully.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a60e98c5-e0a9-4e70-8f00-e57db5161294

📥 Commits

Reviewing files that changed from the base of the PR and between 6af03be and 2476c83.

📒 Files selected for processing (1)
  • libs/widget-lib/src/widgetIframeLoading.ts

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploying swap-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: e988bb3
Status: ✅  Deploy successful!
Preview URL: https://f22e189e.swap-dev-5u6.pages.dev
Branch Preview URL: https://feat-fix-widget-retry-logic.swap-dev-5u6.pages.dev

View logs

@shoom3301

Copy link
Copy Markdown
Collaborator

@Danziger the button doesn't work to me.
When I click it, it just does nothing.

kkklk.mov

@Danziger

Copy link
Copy Markdown
Contributor Author

@shoom3301 The widget configurator preview sets a strict CSP (script-src 'self'), and srcdoc iframes inherit the parent page's policy. I could fix it adding unsafe-inline, but that will only fix it for the configurator. Once an integrator adds it to their site, it will work or not working depending on their CSP policy.

I'd park this until the widget configurator revamp PR is merged, as I have some changes there that show a loader while the iframe loads (added from the app (widget configurator app in this case) context). I'll try this change in there later and re-assess the fix and/or whether we need/want to fix it.

@elena-zh elena-zh left a comment

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.

Hey @Danziger , thank you. I can see the 'Retry' button now.
But it does nothing when I click on it, and I don't see 'loading' state after :(
image

@fairlighteth

Copy link
Copy Markdown
Contributor
Security Review (Codex GPT-5, worked 30m): no verified security findings

Review completed. I found no verified security findings worth posting.

Review scope and related context
  • Checked: libs/widget-lib/src/widgetIframeLoading.ts, the widget postMessage retry path, hidden probe iframe readiness flow, srcdoc error document generation, loadingErrorStyles interpolation, iframe sandbox/referrer/allow attributes, widgetIframeTransport.listenToMessageFromWindow(), and the immediate createCowSwapWidget() READY consumer.
  • Source: code inspection only.
  • Lifecycle: no verified-vulnerability findings.
  • Existing comments: current PR comments already cover the retry flow failing under CSP and the local-origin probe READY race; I did not duplicate those as security findings.
  • Confidence limits: I did not run browser QA or DeepSec. This pass was scoped to verifying concrete security exploitability from the current diff.

Generated using the security-review skill from the CoW Protocol skills repo.

Comment thread libs/widget-lib/src/widgetIframeLoading.ts Outdated
@cowprotocol cowprotocol deleted a comment from coderabbitai Bot Jun 23, 2026
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f08f721-6910-4c4a-947a-97a523981983

📥 Commits

Reviewing files that changed from the base of the PR and between 2476c83 and e988bb3.

📒 Files selected for processing (2)
  • apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx
  • apps/widget-configurator/src/components/configurator/configurator.component.tsx

Walkthrough

widgetIframeLoading now uses probe iframe readiness checks and postMessage-based retry handling. The widget configurator also forwards an optional loading-error callback into the preview widget.

Changes

Widget iframe probe loading and preview callback wiring

Layer / File(s) Summary
Message contracts and loading state
libs/widget-lib/src/widgetIframeLoading.ts
Added retry message validation, transport keys, and listener typing for the iframe loading flow.
Probe-based load orchestration
libs/widget-lib/src/widgetIframeLoading.ts
Refactored the loading flow to track cancellation and loaded state, run primary and probe timeouts, create a hidden probe iframe, handle READY messages, and react to retry messages from the error document.
Error document rendering
libs/widget-lib/src/widgetIframeLoading.ts
Added inline-style sanitization and replaced the static error document with generated retry UI markup and inline button messaging.
Loading error callback wiring
apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx, apps/widget-configurator/src/components/configurator/configurator.component.tsx
Added an optional loading-error callback to the widget props and passed a local configurator callback into the widget preview.

Sequence Diagram(s)

sequenceDiagram
  participant Parent
  participant MainIframe
  participant ProbeIframe
  participant ErrorDocument
  participant widgetIframeTransport
  Parent->>MainIframe: Start primary load timeout
  MainIframe->>Parent: Timeout fires
  Parent->>ErrorDocument: Render error srcdoc
  ErrorDocument->>Parent: Post WIDGET_LOAD_RETRY
  Parent->>ProbeIframe: Create hidden probe iframe
  ProbeIframe->>widgetIframeTransport: Navigate and emit READY
  widgetIframeTransport->>Parent: Deliver READY message
  Parent->>MainIframe: Clear srcdoc and reset src
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • alfetopito
  • fairlighteth

Poem

🐰 A probe hopped out when timeouts grew long,
Retry bells rang in a postMessage song.
Styled errors now speak with a careful grin,
And preview callbacks whisper when loading fails within.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding iframe loading retry logic to handle failures and enable reloading.
Description check ✅ Passed The description covers the summary, testing steps, and background context about the core issue (rebinding listeners after contentWindow changes), following most template sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fix-widget-retry-logic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

* Custom CSS appended to the error document displayed inside the iframe when the widget fails to load.
* Use it to override the default look (`.errorContent` and `.reloadButton` classes).
*/
loadingErrorStyles?: string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since the loading error DOM is on the host side, they can style it using css selector .cow-widget-loading-error

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx`:
- Around line 59-73: The LazyPinnedCowSwapWidget function is missing the
onLoadingError prop in its destructuring and is not forwarding it to the
LazyCowSwapWidget component. Add onLoadingError to the destructuring of
VersionedCowSwapWidgetProps in the LazyPinnedCowSwapWidget function parameters,
and then pass onLoadingError as a prop to the LazyCowSwapWidget component
alongside the existing params, provider, listeners, and onReady props. This
ensures error callbacks are properly propagated for all pinned widget versions,
not just local ones.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f08f721-6910-4c4a-947a-97a523981983

📥 Commits

Reviewing files that changed from the base of the PR and between 2476c83 and e988bb3.

📒 Files selected for processing (2)
  • apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx
  • apps/widget-configurator/src/components/configurator/configurator.component.tsx

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx`:
- Around line 59-73: The LazyPinnedCowSwapWidget function is missing the
onLoadingError prop in its destructuring and is not forwarding it to the
LazyCowSwapWidget component. Add onLoadingError to the destructuring of
VersionedCowSwapWidgetProps in the LazyPinnedCowSwapWidget function parameters,
and then pass onLoadingError as a prop to the LazyCowSwapWidget component
alongside the existing params, provider, listeners, and onReady props. This
ensures error callbacks are properly propagated for all pinned widget versions,
not just local ones.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f08f721-6910-4c4a-947a-97a523981983

📥 Commits

Reviewing files that changed from the base of the PR and between 2476c83 and e988bb3.

📒 Files selected for processing (2)
  • apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx
  • apps/widget-configurator/src/components/configurator/configurator.component.tsx
🛑 Comments failed to post (1)
apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx (1)

59-73: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Forward onLoadingError through the pinned widget path.

onLoadingError is part of widgetProps (Line 86) but gets dropped in LazyPinnedCowSwapWidget (Lines 65 and 72). That makes the callback work only for sdkVersion === 'local' and breaks preview error callback propagation for pinned versions.

Suggested fix
 function LazyPinnedCowSwapWidget({
   sdkVersion,
   params,
   provider,
   listeners,
   onReady,
+  onLoadingError,
 }: VersionedCowSwapWidgetProps & { sdkVersion: PinnedWidgetSdkVersion }): ReactNode {
   const LazyCowSwapWidget = LAZY_WIDGETS_BY_VERSION[sdkVersion]
   const legacyIframeLoadReveal = widgetSdkVersionSupportsReadyEvent(sdkVersion) ? undefined : onReady

   return (
     <Suspense fallback={null}>
       <LegacyPinnedPreviewReveal onIframeLoad={legacyIframeLoadReveal}>
-        <LazyCowSwapWidget params={params} provider={provider} listeners={listeners} onReady={onReady} />
+        <LazyCowSwapWidget
+          params={params}
+          provider={provider}
+          listeners={listeners}
+          onReady={onReady}
+          onLoadingError={onLoadingError}
+        />
       </LegacyPinnedPreviewReveal>
     </Suspense>
   )
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

function LazyPinnedCowSwapWidget({
  sdkVersion,
  params,
  provider,
  listeners,
  onReady,
  onLoadingError,
}: VersionedCowSwapWidgetProps & { sdkVersion: PinnedWidgetSdkVersion }): ReactNode {
  const LazyCowSwapWidget = LAZY_WIDGETS_BY_VERSION[sdkVersion]
  const legacyIframeLoadReveal = widgetSdkVersionSupportsReadyEvent(sdkVersion) ? undefined : onReady

  return (
    <Suspense fallback={null}>
      <LegacyPinnedPreviewReveal onIframeLoad={legacyIframeLoadReveal}>
        <LazyCowSwapWidget
          params={params}
          provider={provider}
          listeners={listeners}
          onReady={onReady}
          onLoadingError={onLoadingError}
        />
      </LegacyPinnedPreviewReveal>
    </Suspense>
  )
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/widget-configurator/src/components/VersionedCowSwapWidget/VersionedCowSwapWidget.tsx`
around lines 59 - 73, The LazyPinnedCowSwapWidget function is missing the
onLoadingError prop in its destructuring and is not forwarding it to the
LazyCowSwapWidget component. Add onLoadingError to the destructuring of
VersionedCowSwapWidgetProps in the LazyPinnedCowSwapWidget function parameters,
and then pass onLoadingError as a prop to the LazyCowSwapWidget component
alongside the existing params, provider, listeners, and onReady props. This
ensures error callbacks are properly propagated for all pinned widget versions,
not just local ones.

@Danziger

Danziger commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

I tried:

  • Enter an invalid baseUrl so that I get the error screen.

  • Once you see the error screen, go to Layout > Presets and click any of them.

  • The styles are incorrectly applied to the iframe instead of being applied to its wrapper:

    image
  • Click the reload widget button at the bottom of the widget configurator sidebar.

  • The error is shown and the iframe is not, but not in the right place according to the current iframeStyle.

  • Click Reload in the error screen.

  • The error screen goes away and the iframe is incorrectly displayed with a loading error (or a Cloudfare error in this case cause I just added an invalid URL from them):

    image

I think what's missing is that iframeStyle should be renamed to rootStyle and applied to the container, not to the iframe. As we haven't released this yet, I don't think there's any reason to keep iframeStyle or allow directly styling the iframe as long as we add some sensible defaults for it (display: block, border: none at least).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants