Muin is in private beta.Watch the public release announcement —talk to us.
Falaah Falaah AI

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:

  1. Modal embeds: Focus a button inside the modal first (Close, or the modal title), not the iframe. Browsers respect autofocus on the <dialog> itself.
  2. 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:

  1. Check curl -sI output (above) — cross-origin-resource-policy: cross-origin must be present.
  2. Confirm Stripe is running in production-domain mode, not test mode with restricted origins.
  3. 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:

  1. Verify the response is fresh — curl -sI <url> should show x-cache: Miss/Hit/RefreshHit from cloudfront. If it shows x-cache: Error from cloudfront, the CDN deploy didn’t fully land — contact ops.
  2. Force-reload the host page (Cmd-Shift-R / Ctrl-Shift-F5) to clear the browser cache.
  3. 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:

  1. CloudFront EmbeddableHeadersPolicy is attached to /go/* and /embed/* cache behaviors (Plan 352 / muin.devops/cdk/lib/cloudfront-stack.ts).
  2. Caddy’s @embeddable_paths matcher matches your test URL (muin.devops/caddy/Caddyfile).
  3. The middleware predicate is_embeddable() returns true for 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 -sI output for the failing URL.
  • For Stripe-specific issues, include the stripe.js version in use (visible in DevTools Network tab).

Next