Skip to content

Legacy-iframe fallback renders broken-image placeholder instead of embedded admin (1.10.0) #673

@martin-castro-laminr-ai

Description

@martin-castro-laminr-ai

Symptom

The legacy-iframe escape hatch from #659 ships in 1.10.0 and correctly detects a ModelAdmin with a custom `change_form_template` — the SPA shell renders the breadcrumb, title, and the helper strip:

This form is rendered by the legacy admin (custom change_form_template). [Open in new tab] [Cancel]

…but the iframe body itself is blank with a broken-image placeholder icon (the document-with-X glyph browsers show when an embedded resource fails to load). The legacy admin page never renders inside the iframe.

"Open in new tab" works fine — same legacy URL loads normally as a top-level navigation.

Probable cause

One of the following — needs to be confirmed by inspecting the iframe's Network / Console tab:

  1. CSP `frame-ancestors` / `X-Frame-Options: DENY` on the legacy admin response. Django's stock admin doesn't set these by default, but most projects add `django.middleware.clickjacking.XFrameOptionsMiddleware` which sets `X-Frame-Options: DENY` globally. That blocks the iframe and the browser falls back to the broken-image icon.
  2. The iframe `src` doesn't match the SPA's origin and the legacy admin's session cookie isn't `SameSite=None; Secure`, so the iframe loads unauthenticated and the legacy page rejects with a redirect chain the browser then refuses to display.
  3. The SPA passes an empty / wrong `src` (escape-hatch payload missing `legacy_url` for this code path) — the iframe element exists but points nowhere.

Acceptance criteria

  • Iframe `src` is the same URL surfaced by "Open in new tab" (verify by inspecting DOM).
  • When the legacy backend serves the page with `X-Frame-Options: SAMEORIGIN` and the SPA is on the same origin, the iframe renders successfully.
  • When the legacy backend refuses to be framed (DENY or cross-origin), the SPA detects the load failure and renders a clear fallback — "Embedding refused by the legacy admin. Open in new tab." — not the browser's broken-image icon.
  • Document the required backend headers in the README / integration guide:
    • `X-Frame-Options: SAMEORIGIN` (or remove `XFrameOptionsMiddleware` entirely)
    • if cross-origin between SPA and legacy admin: `Content-Security-Policy: frame-ancestors ` on the legacy responses, and session cookie set `SameSite=None; Secure`
  • Add a fixture in `examples/` that exercises a ModelAdmin with a `change_form_template` override (e.g. `examples/jobs/job//change/?run_custom=1` rendering a dual-listbox template) so the iframe path can be exercised end-to-end against the example backend.
  • Visual snapshot of the iframe-fallback page showing the embedded legacy form actually rendered (not the broken-image icon).

Detection of load failure

Iframes don't fire a meaningful `error` event when the response sets X-Frame-Options. The reliable detection pattern is:

```tsx
const onLoad = () => {
try {
// Same-origin: this access succeeds.
void iframe.contentDocument?.body;
setStatus("loaded");
} catch {
// Cross-origin: opaque, but the iframe loaded something.
setStatus("loaded");
}
};
const timeout = setTimeout(() => {
if (status === "loading") setStatus("refused"); // never fired onload
}, 4000);
```

Or, more directly: have the backend's form-spec response include a `legacy_iframeable: boolean` flag computed server-side from the response middleware chain, and switch to the "Open in new tab only" UI immediately when `false`.

Screenshot

User screenshot on stage /admin2/files/file/<pk>/change/?reprocess_parsers=1 attached in the originating chat — SPA shell + helper strip rendered; iframe body shows the broken-image placeholder; no legacy form visible.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions