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:
- 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.
- 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.
- 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
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
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:
…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:
Acceptance criteria
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=1attached in the originating chat — SPA shell + helper strip rendered; iframe body shows the broken-image placeholder; no legacy form visible.Related