Skip to content

WIP: Fix Gutenberg site editor under Document-Isolation-Policy#3320

Closed
adamziel wants to merge 1 commit intotrunkfrom
fix/dip-gutenberg-site-editor
Closed

WIP: Fix Gutenberg site editor under Document-Isolation-Policy#3320
adamziel wants to merge 1 commit intotrunkfrom
fix/dip-gutenberg-site-editor

Conversation

@adamziel
Copy link
Copy Markdown
Collaborator

@adamziel adamziel commented Feb 27, 2026

Summary

Gutenberg 26.2 uses SharedArrayBuffer in the site editor. That requires cross-origin isolation. WordPress serves the editor frame with COEP/COOP headers. It's pretty difficult to do the same in Playground, so instead we rewrite those headers as Document-Isolation-Policy. However, DIP has subtle side effect that breaks how Gutenberg sets up its editor canvas. I think (?) it wasn't a problem at the time #3028 was merged, but Gutenberg internals changed in WordPress/gutenberg#74418 (and likely a few other PRs) so the situation needs to be re-evaluated now.

This is a WIP branch capturing the debugging so far.

Why Playground can't use COEP/COOP

COEP/COOP require the entire frame ancestry to participate. To get SharedArrayBuffer inside the site editor iframe, headers must be set on:

  • The site editor iframe itself
  • The remote.html Playground frame above it
  • The playground.wordpress.net host page above that
  • Any third-party page that embeds Playground

That last point is the blocker. Playground is embedded on WordPress.org, in documentation, in tutorials. Those pages don't send COEP/COOP, and we can't make them.

Native WordPress doesn't have this problem — wp-admin does full-page reloads, so COEP/COOP only needs to cover the current page. In Playground, the top-level frame stays alive for the entire session.

Perhaps we could introduce a query param such as ?coop=1 or so and use a magic default value depending on whether Playground is loaded from an iframe, but that sounds fragile.

The DIP problem

Document-Isolation-Policy isolates a single document without requiring parent cooperation. But it has a catch:

When only the child frame has DIP but the parent doesn't:

  • window.frameElement becomes null
  • Parent-to-child contentDocument access is blocked

Gutenberg's site editor calls iframe.contentWindow.contentDocument directly to set up the block editor canvas. When contentDocument is blocked, it crashes.

When both parent and child have DIP:

  • window.frameElement is defined
  • contentDocument access works
dip-dual-parent-child

What this branch does

Serves Document-Isolation-Policy: isolate-and-credentialless on all HTML responses in Playground — from the service worker and from both Vite dev servers — so that the parent-child DIP requirement is satisfied.

To try it, go to http://localhost:5400/website-server/?wp=beta&url=%2Fwp-admin%2Fsite-editor.php#ewogICJwbHVnaW5zIjogW10sCiAgInN0ZXBzIjogW10sCiAgInByZWZlcnJlZFZlcnNpb25zIjogewogICAgInBocCI6ICI4LjMiLAogICAgIndwIjogImJldGEiCiAgfSwKICAiZmVhdHVyZXMiOiB7fSwKICAibG9naW4iOiB0cnVlLAogICJsYW5kaW5nUGFnZSI6ICIvd3AtYWRtaW4vc2l0ZS1lZGl0b3IucGhwIgp9

It has some very rough edges, e.g. all the blocks crash and I don't know why. Also, I'm not sure about the impact of this on Playgrounds embedded on sites without the Document-Isolation-Policy header. Also, it further complicates the Chrome vs FF/Safari maintenance split. Finally, I'm not sure if client-side media processing actually works in this PR. But I know SharedArrayBuffer is available in the right iframe, and that's already a lot. This PR is just a debugging exploration to understand the phenomenon. It's not meant for merging.

cc @brandonpayton @JanJakes @adamsilverstein @swissspidy

related to #2954

…Gutenberg site editor

Gutenberg 26.2 uses SharedArrayBuffer in the site editor, which requires
cross-origin isolation. Playground achieves this via Document-Isolation-Policy
(DIP) instead of COEP/COOP because the entire parent frame chain would need
COEP/COOP headers – including third-party sites that embed Playground.

However, when only the child iframe has DIP but the parent doesn't,
window.frameElement becomes null and contentDocument access is blocked.
Gutenberg's site editor directly manipulates iframe.contentDocument to build
the editing canvas, so it breaks.

The fix is to serve DIP on all HTML responses in Playground – both from the
service worker and from the Vite dev servers. When both parent and child frames
have DIP, frameElement and contentDocument access work correctly.

This is WIP/exploratory – several guards are commented out and debug logging is
present.
@adamziel adamziel closed this Feb 27, 2026
@adamziel
Copy link
Copy Markdown
Collaborator Author

adamziel commented Feb 27, 2026

I'm closing this since it's not meant for merging. It's more of a demonstration of the problem. I won't be able to keep advancing this stream of work, please feel free to take over.

*/
add_filter('wp_client_side_media_processing_enabled', '__return_false');
// add_filter('wp_client_side_media_processing_enabled', '__return_false');
define( 'DISABLE_WP_CRON', true );
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

it's so slow on beta-2, I don't know why. Should we disable it entirely until we have multi-worker in the browser? @brandonpayton

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.

@adamziel did you test with the #3306 fix? It reduces the number of situations where requests are waiting on cron jobs.

@adamsilverstein
Copy link
Copy Markdown
Member

@bgrgicak / @adamziel - Would it help Pl;yground using DIP if we use DIP in core/Gutenberg instead of COEP/COOP? I am proposing we do that and support firefox/safari via a plugin. See WordPress/wordpress-develop#11098 / WordPress/gutenberg#75991

@adamziel
Copy link
Copy Markdown
Collaborator Author

I think so @adamsilverstein, thank you so much for following up here!

bgrgicak pushed a commit that referenced this pull request Apr 23, 2026
…n-Policy (#3515)

## Motivation for the change, related issues

Fixes #3514.

Playground currently disables Gutenberg's **client-side media
processing** experiment via an `__return_false` filter in the mu-plugin
([0-playground.php#L265-L274](https://github.com/WordPress/wordpress-playground/blob/trunk/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php#L265-L274)).
That workaround was added in #3312 because, at the time, Gutenberg was
emitting COEP/COOP for the editor and — even when rewritten to
`Document-Isolation-Policy` by the service worker — the editor's inner
iframe was crashing.

[Gutenberg PR #75991](WordPress/gutenberg#75991)
(shipped in 22.6+) changed the isolation strategy: on Chromium 137+
Gutenberg now sends `Document-Isolation-Policy:
isolate-and-credentialless` **directly** on editor screens and no longer
sends COEP/COOP. That removes the root cause of the breakage — but it
also means Playground's existing COEP→DIP rewrite path never fires on
current Gutenberg, so the scope is never added to
`scopesWithCrossOriginIsolation`, `empty.html` doesn't receive DIP, and
parent/child DIP parity breaks inside the block editor canvas (see #3320
for why parity matters).

## Implementation details

Two small, coordinated changes:

1. **`packages/playground/remote/service-worker.ts`** — broaden the
post-response handler (now `applyCrossOriginIsolationHeaders`) so that:
- If a response already carries `Document-Isolation-Policy`, the scope
is added to `scopesWithCrossOriginIsolation` and the response passes
through unchanged. This is the modern path once Gutenberg serves DIP
directly.
- If a response carries COEP/COOP, the existing rewrite path continues
to operate unchanged (covers older Gutenberg, WP core's
`wp_set_up_cross_origin_isolation`, or custom plugins).

The existing `empty.html` branch (which adds DIP to the inner editor
iframe when the scope is tracked) needs no change — it now gets
populated in both cases.

2.
**`packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php`**
— remove `add_filter('wp_client_side_media_processing_enabled',
'__return_false')` and its comment block. The workaround is no longer
necessary.

## Tests

New Playwright spec:
`packages/playground/website/playwright/e2e/client-side-media.spec.ts`

- Chromium-only (skipped in Firefox/Safari — DIP and client-side media
are Chromium-only today).
- Boots Playground with Gutenberg + `gutenberg-media-processing`
experiment on `/wp-admin/post-new.php` and asserts, inside the WP admin
iframe:
  - `window.crossOriginIsolated === true`
  - `typeof SharedArrayBuffer !== 'undefined'`
- `window.__clientSideMediaProcessing === true` (Gutenberg's own
enablement flag)

Editor rendering under DIP is already covered by the existing
`document-isolation-policy.spec.ts` suite, which implicitly verifies
parent/child DIP parity.

## Testing instructions

Manual (Chromium ≥ 137):

1. Run `npm run dev` and open `http://127.0.0.1:5400/website-server/`.
2. Load Guetnberg with this Blueprint (paste into the URL after `#`):

```json
{
  "$schema": "https://playground.wordpress.net/blueprint-schema.json",
  "landingPage": "/wp-admin/post-new.php",
  "plugins": ["gutenberg"],
  "login": true,
  "steps": [
    {
      "step": "runPHP",
      "code": "<?php require '/wordpress/wp-load.php'; update_option('gutenberg-experiments', array('gutenberg-media-processing' => true));"
    }
  ]
}
```

3. The post editor loads without crashing. 
4.  In the WP iframe's DevTools console:
   - `crossOriginIsolated` → `true`
   - `typeof SharedArrayBuffer` → `"function"`
   - `window.__clientSideMediaProcessing` → `true`
5. The admin document response carries `Document-Isolation-Policy:
isolate-and-credentialless` and no COEP/COOP.
6. Upload an image — it is processed client-side (wasm-vips loads; no
server-side sub-size generation request).
7. Upload a small AVIF image (sample attached below) - it works (before
this PR it fails)
8. Non-Chromium (Firefox/Safari): editor still opens; client-side media
is simply not engaged (unchanged behavior).
9. Embedded Playground on a 3rd-party page with no COEP/COOP still loads
(DIP is per-document, so this should hold).
10. Embedding 3p elements on the page, eg. a YouTube embed, still work
11. Iframe using elements such as the classic block work as expected

Automated:

```
npx nx e2e playground-website --grep="Document-Isolation-Policy|client-side media|crossOriginIsolated"
```

## Notes on backwards compatibility

- No user-facing breaking changes.
- The COEP→DIP rewrite path is preserved, so older WordPress/Gutenberg
versions, or plugins that set COEP/COOP themselves, keep working as they
do today.
- Non-Chromium browsers: no regression — DIP is unsupported there,
Playground's feature detection returns `false`, headers pass through,
and Gutenberg's JS feature detection falls back the same as before.
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.

3 participants