Skip to content

YouTube Dead in Production: The Referer the Player Needed

Adityo Guni Waluyo

The YouTube iframe worked locally and died on deploy: a same-origin Referrer-Policy stripped the referer the player needs, fixed per element.

TL;DR

YouTube iframes were broken. The issue was caused by a Referrer-Policy header. Adding a referrerPolicy attribute to each iframe fixed it.

The YouTube player on the homepage showed up as a dark rectangle with no play button, in production. On my local machine the same build behaved: poster rendered, play button present, video started. I moved the tab to production, refreshed, and got the same dead box. No console error, no red request in the network tab, just a player that refused to come alive.

What made it confusing: every other asset was fine. Images, fonts, JSON from the API, all 200. Only the YouTube iframes were broken, and they broke identically in three places at once: the per-entity video embed, the playlist, and the hero slider on the front page.

First guess: something blocked the iframe. I checked the response headers, no X-Frame-Options forbidding it, no CSP cutting frame-src. Second guess: wrong video id, or a private video. I opened the embed URL directly in a new tab and the video played. Third guess: adblock in my browser. Clean browser profile, same result.

The header that cuts the referer, and a player that needs an identity

What opened the path was not any of those three guesses, but a different question: what differs between my local machine and production? The least expected answer was a response header. The production edge sends Referrer-Policy: same-origin. Locally that header is absent entirely, so the browser falls back to its own default.

The same-origin directive means the browser still sends path and query for same-origin requests, but sends no Referer header for cross-origin requests [1]. A YouTube iframe is a cross-origin request, so in practice no referer went out.

The problem is that the player wants that referer. YouTube API Services documentation states that clients using the embedded player must provide identification through the HTTP Referer header, and that clients should not set Referrer-Policy in a way that suppresses the referer value. The official recommendation is written out explicitly: strict-origin-when-cross-origin [3]. Without that identity the player fails to configure itself and stops at an empty screen.

Why was local fine? Because no header suppressed the referer there, so the browser dropped to the strict-origin-when-cross-origin default. That default still sends the origin for cross-origin requests, which is enough for the player [1][5]. Chrome moved its own default to this policy back in version 85, precisely to cut path and query leakage between origins, not to kill the referer entirely [2].

One attribute per iframe, not a looser site header

The fix never touched server configuration. I added a referrerPolicy attribute to each YouTube iframe, set to strict-origin-when-cross-origin. The referrerpolicy attribute on an iframe element declares which referrer is sent when fetching that frame's resource [4], so the element-level policy governs that frame's request.

const embed = (
  <iframe
    src={`https://www.youtube-nocookie.com/embed/${videoId}`}
    title="Video YouTube"
    referrerPolicy="strict-origin-when-cross-origin"
    allow="encrypted-media; picture-in-picture"
    allowFullScreen
    className="aspect-video w-full"
  />
);

I deliberately did not loosen the site header. That same-origin header still earns its place on other pages: it holds back path and query so they do not leak to third parties. Replace the global header to save one iframe and the whole site starts shipping more referrer data. An element-level attribute has a far narrower blast radius, and it applies only to the frame that actually needs it.

Verification in production: after deploying, the box that had been dark turned into a poster with a working play button, with zero changes in nginx or Cloudflare. The three broken surfaces recovered together, which matched the assumption that there was one cause, not three.

One detail I kept: the embeds still use the youtube-nocookie.com domain, so YouTube cookies are not set before the video actually plays. Referrer policy and embed domain are two separate things, and both can be used at once.

The lesson I carried out: security headers are scoped per fetch, not per site. The header that protects a page can be the exact header that starves a third-party frame of its identity, and the precise fix usually sits at the element that breaks, not at the server that applies to everything.

Sources

Related articles