Troubleshooting Embed Errors
Diagnose CSP errors, frame-ancestors blocks, sizing issues, focus stealing, and Stripe Elements double-nest problems in Muin embeds.
If a Muin embed isn’t loading or behaving correctly, most issues fall into one of these categories. Each section explains the symptom, root cause, and fix.
”Refused to display in a frame because of frame-ancestors”
Symptom (browser DevTools console):
Refused to display ‘https://muin.falaah.ai/…’ in a frame because an ancestor violates the following Content Security Policy directive: “frame-ancestors ‘none’”.
Cause: The path you’re embedding is not on the embeddable allowlist.
Only /go/*, /embed/*, and /api/v1/embed/* paths are framable.
Anything else — including dashboards, settings, or /api/v1/... — is
deliberately blocked.
Fix: Use the public surface of the resource. For a form, use
/go/<tenant>/form/<slug> (not /forms/<id> which is the authenticated
admin view).
Verify the response:
curl -sI "https://muin.falaah.ai/go/<tenant>/form/<slug>" | grep -iE "x-frame-options|content-security-policy|cross-origin-resource-policy"
Embeddable paths return:
content-security-policy: ... frame-ancestors *; ...
cross-origin-resource-policy: cross-origin
x-frame-options: ALLOWALL
Non-embeddable paths return:
content-security-policy: ... frame-ancestors 'none'; ...
cross-origin-resource-policy: same-origin
x-frame-options: DENY
“Refused to load script” (CSP block on host page)
Symptom: The iframe loads, but the host page’s CSP rejects it:
Refused to frame ‘https://muin.falaah.ai/’ because it violates the following Content Security Policy directive: “frame-src ‘self’”.
Cause: Your host page has its own CSP that doesn’t allow framing from
muin.falaah.ai.
Fix: Add Muin to your CSP’s frame-src (or child-src for older
browsers):
Content-Security-Policy: frame-src 'self' https://muin.falaah.ai;
If you’re on staging, also allow https://muin-stg.falaah.ai.
Iframe height collapses to a few pixels
Symptom: The iframe loads but renders at 50–100 px tall instead of the form’s natural height.
Cause: Iframes have no intrinsic height. Without an explicit height
attribute or CSS rule, browsers fall back to a default that’s almost
always wrong for a form.
Fix: Set a fixed height that fits the tallest expected form state. The
recommended default is height="700". For donation pages with payment
fields, height="850" is safer.
<iframe src="..." width="100%" height="700"></iframe>
Dynamic auto-resizing (postMessage-based) is not supported today — the trade-off is one less moving part for integrators.
Iframe steals focus from the host page
Symptom: Visitor scrolls down, an iframe pops into view, and suddenly the keyboard cursor jumps inside the iframe — typing on the host page goes into the embed.
Cause: The iframe (or its first form field) auto-focused on load. If the iframe is inside a modal or scroll-triggered panel, the auto-focus fires when the panel opens.
Fix: Don’t let the iframe steal focus until the visitor explicitly interacts with it. Two patterns:
- Modal embeds: Focus a button inside the modal first (Close, or the
modal title), not the iframe. Browsers respect
autofocuson the<dialog>itself. - Scroll-triggered: Don’t programmatically
focus()the iframe. Visitors will tab into it themselves.
If the form’s own auto-focus is the culprit, you can suppress it on the host side by adding a sentinel that grabs focus first:
<button
className="sr-only"
ref={(el) => el?.focus()}
aria-label="Embed loaded"
></button>
<iframe src="..." />
Stripe Elements double-nest error
Symptom: When embedding a donation form that uses Stripe Elements, the card field shows:
An error occurred. The Card element you’re using is in an iframe.
Cause: Stripe Elements itself runs in an iframe. Embedding a Muin
donation page (which loads Stripe Elements inside it) creates a
double-nested iframe — host > muin > stripe. Stripe’s JS detects the
nesting and refuses to render the card field by default.
Fix: This works automatically when both layers serve
Cross-Origin-Resource-Policy: cross-origin (which Plan 352 ensures).
No host-side change is needed — verify by reloading the page after the
Plan 352 deploy. If it still fails:
- Check
curl -sIoutput (above) —cross-origin-resource-policy: cross-originmust be present. - Confirm Stripe is running in production-domain mode, not test mode with restricted origins.
- Verify the host page is HTTPS — Stripe will not load over HTTP even with correct headers.
Cookies / session not persisting in the embed
Symptom: Each form submit acts like a brand-new session — no return-visitor recognition, no pre-fill from a prior submission.
Cause: Modern browsers (Safari ITP, Chrome 3rd-party-cookie phase-out,
Firefox Total Cookie Protection) block third-party cookies by default. A
Muin embed on your-site.com cannot read or write cookies on
muin.falaah.ai from the iframe context.
Fix: Don’t rely on cookies for cross-origin embeds. The Muin form identifies submissions by the body fields (email, phone) and any hidden fields you pre-fill via query string. If you need a session-bound flow, host the form same-origin (not embedded).
Fonts / styling look “off”
Symptom: The embed renders but text is unstyled, looks like serif/Times New Roman, or fonts are blocked in DevTools.
Cause: Muin uses self-hosted fonts served from the same CloudFront
distribution. If the host page’s CSP forbids font-src 'self', the host
CSP doesn’t apply to the iframe’s content (different origin), so this is
unlikely to be host-side.
Fix (rare): If you’ve configured an enterprise reverse proxy that
strips font-src directives from muin.falaah.ai responses, you’ll
need to allow them through. This is not a bug we can fix on the Muin
side.
”Headers are right on direct curl, but iframe still blocked”
Symptom: curl -sI shows frame-ancestors * and no X-Frame-Options,
but the browser console still complains about the embed being blocked.
Cause: Browser is hitting a stale cached version of the page from before Plan 352 deployed. CloudFront edge nodes can hold cached responses for several minutes after a config update; browser caches add another layer.
Fix:
- Verify the response is fresh —
curl -sI <url>should showx-cache: Miss/Hit/RefreshHit from cloudfront. If it showsx-cache: Error from cloudfront, the CDN deploy didn’t fully land — contact ops. - Force-reload the host page (Cmd-Shift-R / Ctrl-Shift-F5) to clear the browser cache.
- If the issue persists across browsers, run a CloudFront cache
invalidation on
/go/*and/embed/*.
Embed works on dev (localhost:3000) but breaks on production
Symptom: npm run dev shows the embed loading and submitting fine.
After deploy, the embed shows an empty white iframe or a 403.
Cause: Local dev uses Caddy as a reverse proxy (caddy/Caddyfile),
which sets the same headers as muin.server’s middleware. Production
goes through CloudFront, which has its own response-headers policy.
If only one of these layers is updated, the headers diverge between
local and prod.
Fix: Verify the production curl output (above section). If headers
differ between local and prod, check:
- CloudFront
EmbeddableHeadersPolicyis attached to/go/*and/embed/*cache behaviors (Plan 352 /muin.devops/cdk/lib/cloudfront-stack.ts). - Caddy’s
@embeddable_pathsmatcher matches your test URL (muin.devops/caddy/Caddyfile). - The middleware predicate
is_embeddable()returnstruefor your path (muin.server/src/muin_server/api/middleware/response_headers.py).
See the operator runbook for the full debug flow when prod diverges from staging.
Still stuck?
- File the issue with the failing URL, the host page (or a minimal reproduction), the browser version, and the full DevTools console output.
- Include the
curl -sIoutput for the failing URL. - For Stripe-specific issues, include the
stripe.jsversion in use (visible in DevTools Network tab).