ProxDocs

Cross-origin isolation

If you take one thing from this page: send these two headers. Skipping them breaks a whole class of proxied sites, and the failure mode gives you almost no useful information.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Why it is needed

Setting these headers makes your page cross-origin isolated, which is what grants access to SharedArrayBuffer. That much is ordinary web platform behaviour. The part that matters for a proxy is what happens next: when your shell is isolated, Scramjet stamps Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp onto every proxied document, iframe, worker, sharedworker, script and stylesheet on the way back out.

Isolation propagates into the sites you proxy:

Your server sends COOP + COEP
  → your shell is cross-origin isolated
    → the engine re-sends both headers on every proxied response
      → proxied pages are isolated too
        → sites that need SharedArrayBuffer actually work
          → and all of it needs a secure context, so HTTPS

Skip the headers and that chain never starts. The engine itself keeps running, since Scramjet's wasm rewriter is single-threaded and does not use SharedArrayBuffer. What you lose is every proxied site that wants it: ffmpeg.wasm video tools, emulators, wasm-threaded ML, some editors.

Be realistic about the size of that set. Most sites never touch SharedArrayBuffer, and many libraries that use it fall back to a single-threaded path instead of failing, so the usual symptom is "much slower than it should be" rather than a hard break. When it does break, it breaks from inside the frame, in somebody else's minified code, with nothing pointing back at a header you did not send.

That is the argument for sending them: the cost is two lines and self-hosting your own assets, the failure is silent and unattributable, and you cannot add the headers retroactively for a user who already hit the problem.

There is one place Scramjet needs isolation in your own shell: the syncxhr flag, which is off by default. See config and flags.

Ultraviolet does the same thing, and does it correctly. Its service worker re-sends Cross-Origin-Embedder-Policy: require-corp on proxied responses when the shell is isolated. It never sets Cross-Origin-Opener-Policy, and it does not need to: COOP only applies to top-level documents. On a nested browsing context it is ignored, so an iframe is isolated when the top-level page is isolated and the frame itself carries COEP. That is exactly what UV sends.

Scramjet sets both on proxied responses. The COOP half is doing nothing for frames; it costs nothing either.


What each header does

Cross-Origin-Opener-Policy: same-origin

Severs the relationship between your page and any window that opened it or that you open, unless it is same-origin. window.opener becomes null across origins.

Without this, a cross-origin opener could share your process.

What it breaks: OAuth popups and payment flows that post back to window.opener. If your site has a "Sign in with…" popup, it stops working. same-origin-allow-popups relaxes this for windows you open, but it is not sufficient for isolation, so it is not an option if you need Scramjet.

Cross-Origin-Embedder-Policy: require-corp

Every cross-origin subresource must explicitly opt in to being embedded, by sending Cross-Origin-Resource-Policy: cross-origin or by being fetched with CORS.

Without this, you could embed a cross-origin image and read it through a timing side channel.

What it breaks: every third-party asset that does not send CORP. Google Fonts, a CDN script, an analytics pixel, an external favicon, all blocked, and the console message is not always obvious.

There is a gentler value, credentialless, which sends cross-origin no-cors requests without credentials instead of requiring opt-in. It also grants isolation and breaks less. Support is good in Chromium and Firefox; Safari lagged. require-corp is the safe default; credentialless is worth trying if third-party assets are a problem for you.


Setting them

Express:

app.use((_req, res, next) => {
	res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
	res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
	next();
});

Register it before your static handler and before any proxy routes, so it covers every response including assets. See Other frameworks for Fastify, Hono, Vite, and Next.js.


Checking it worked

In the console of your page:

crossOriginIsolated;

The value must be true. If it is false, SharedArrayBuffer is unavailable to your shell and to everything you proxy. Check in order:

  1. Both headers present on the document response, not just on assets. Look at the Network tab, select the document request, read the response headers.
  2. Secure context. https://, or http://localhost. A LAN address like http://192.168.1.5:8080 is not a secure context and will not isolate.
  3. Nothing stripping them. Cloudflare, an nginx proxy_pass, or a hosting layer can drop or override response headers.
  4. Every embedded resource sends CORP under require-corp, or is same-origin.

The symptoms

The failure is bad because it is silent, late, and happens in someone else's code. Your shell loads. The UI renders. Sites proxy fine. Then one particular site throws SharedArrayBuffer is not defined from inside its own bundle, and nothing in that message mentions a header on your server.

Common presentations:

SymptomCause
SharedArrayBuffer is not defined inside a proxied siteYour shell is not isolated, so the frame is not either
crossOriginIsolated === false but headers look rightNot a secure context, or something stripped them
Works on localhost, breaks in productionHTTPS missing, or your host rewrites headers
Google Fonts / CDN assets 404 or blockedrequire-corp blocking them. Self-host, or try credentialless
OAuth popup can no longer reach window.openerCOOP. Expected; no way around it with full isolation

Check crossOriginIsolated early whenever a specific site misbehaves in a way that mentions wasm or shared memory. It costs one line.

A blank frame or a proxy that does nothing at all is usually not this. Look at service worker scope and registration first, since the engine runs fine without isolation.


Practical consequences

Self-host your assets. Under require-corp, fonts, icons, and scripts from a CDN need CORP headers you do not control. Self-hosting is simpler than fighting it, and faster anyway.

Your proxy page cannot be embedded casually. COOP/COEP constrain how other pages interact with yours. If you intend to embed your proxy in another site, work that out early.

about:blank cloaking interacts with COOP. Opening about:blank and injecting an iframe still works. about:blank inherits the opener's origin. But the isolation relationship is subtle and browser-dependent. Test it rather than assuming. See Running a proxy site well.

Localhost is exempt from HTTPS, not from headers. You still need COOP/COEP in development. The generated servers set them in both cases.

Profile Views