Missing headers when i open app inside iframe

I have 2 apps in my monorepo. App A is assigned domainA.com and App B is assigned domainB.com

Plan is to open domainB.com in an iframe in domainA.com. All this works locally when the 2 domains are on localhost:3000 and localhost:3001, but once deployed, domainB.com is blocked inside the iframe with the following error on Chrome:

Because your site has the Cross-Origin Embedder Policy (COEP) enabled, each resource must specify a suitable Cross-Origin Resource Policy (CORP). This behavior prevents a document from loading cross-origin resources which don’t explicitly grant permission to be loaded.

To solve this, add the following to the resource’ response header:

Cross-Origin-Resource-Policy: same-site if the resource and your site are served from the same site.
Cross-Origin-Resource-Policy: cross-origin if the resource is served from another location than your website. ⚠️If you set this header, any website can embed this resource.
Alternatively, the document can use the variant: Cross-Origin-Embedder-Policy: credentialless instead of require-corp. It allows loading the resource, despite the missing CORP header, at the cost of requesting it without credentials like Cookies.

When I inspect the request to domainB.com when loaded inside iframe, there are no headers in the response. But when i open domainB.com outside an iframe, the response has all the headers required.

next.config.mjs for domainA:

/** @type {import('next').NextConfig} */
const nextConfig = {
  // reactStrictMode: false,  // Uncomment to see if duplicate requests are due to React Strict Mode
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
  async rewrites() {
    return [
      {
        source: "/home",
        destination: "/",
      },
    ];
  },
  experimental: {
    swcPlugins: [
      [
        "@preact-signals/safe-react/swc",
        {
          // you should use `auto` mode to track only components which uses `.value` access.
          // Can be useful to avoid tracking of server side components
          mode: "auto",
        } /* plugin options here */,
      ],
    ],
  },
};

const ContentSecurityPolicy = `
    child-src 'self' https://pykernel.app https://www.pykernel.app http://localhost:3001;
    frame-src 'self' https://pykernel.app https://www.pykernel.app http://localhost:3001;
`;

const securityHeaders = [
  {
    key: "Content-Security-Policy",
    value: ContentSecurityPolicy.replace(/\n/g, ""),
  },
  {
    key: "Cross-Origin-Embedder-Policy",
    value: "credentialless",
  },
  {
    key: "Cross-Origin-Opener-Policy",
    value: "same-origin",
  },
  {
    key: "Cross-Origin-Resource-Policy",
    value: "cross-origin",
  },
];

export default nextConfig;

next.config.mjs for domainB:

/** @type {import('next').NextConfig} */
const nextConfig = {
  // reactStrictMode: false,  // Uncomment to see if duplicate requests are due to React Strict Mode
  pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: securityHeaders,
      },
    ];
  },
};

const ContentSecurityPolicy = `
    default-src 'self' vercel.live;
    script-src blob: 'self' 'unsafe-eval' 'unsafe-inline' cdn.vercel-insights.com vercel.live va.vercel-scripts.com pyscript.net https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/ https://cdn.jsdelivr.net/pyodide/ https://cdn.jsdelivr.net/npm/@pyscript/ https://cdn.jsdelivr.net/npm/@micropython/;
    worker-src 'self' blob: data:;
    style-src * 'self' 'unsafe-inline';
    img-src * blob: data:;
    media-src 'none';
    connect-src * blob:;
    font-src * 'self' data:;
    frame-src 'self' vercel.live;
    frame-ancestors 'self' https://pykernel.com https://www.pykernel.com http://localhost:3000;
`;

const securityHeaders = [
  {
    key: "Content-Security-Policy",
    value: ContentSecurityPolicy.replace(/\n/g, ""),
  },
  {
    key: "Referrer-Policy",
    value: "strict-origin-when-cross-origin",
  },
  {
    key: "X-Content-Type-Options",
    value: "nosniff",
  },
  {
    key: "X-DNS-Prefetch-Control",
    value: "on",
  },
  {
    key: "X-Frame-Options",
    value: "ALLOW-FROM https://pykernel.com https://www.pykernel.com http://localhost:3000",
  },
  {
    key: "Strict-Transport-Security",
    value: "max-age=31536000; includeSubDomains; preload",
  },
  {
    key: "Permissions-Policy",
    value: "camera=(), microphone=(), geolocation=()",
  },
  {
    key: "Cross-Origin-Embedder-Policy",
    value: "credentialless",
  },
  {
    key: "Cross-Origin-Opener-Policy",
    value: "same-origin",
  },
  {
    key: "Cross-Origin-Resource-Policy",
    value: "cross-origin",
  },
];

export default nextConfig;

why does domainB get the headers in the response when opened by itself and why are these headers missing when opened inside an iframe?

Opened in iframe:

Opened standalone:

Hi, @saiyalamarty!

Did you manage to figure this out? Would you like to share with the community? :smile:

Hi @pawlean,

Yes, I figured out the issue. I was opening the domain inside the iframe as domainB.com and not www.domainB.com.

domainB.com was probably redirecting to www inside the iframe because i was getting a 308 Permanent Redirect. The headers were missing during/after this redirect. I was able to resolve the issue by explicitly including www in the iframe url.

1 Like

Thanks for sharing again, @saiyalamarty! :blush:

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.