When a browser loads an HLS playlist from another origin, the server hosting the playlist and segments must allow that request with CORS headers. If it does not, the request is blocked before JavaScript can read the response, and hls.js reports a network error such as MANIFEST_LOAD_ERROR or FRAG_LOAD_ERROR. The fix always happens on the server that serves the files, never in the player.
This affects JavaScript-based playback (hls.js, Shaka, most web players). Safari's native HLS playback is handled by the operating system and follows different rules, which is why a stream can work on an iPhone but fail in Chrome on the same machine.
Open DevTools and look at the Network tab while loading the stream. A CORS failure usually shows the request in red with status (failed) or CORS error, and the Console contains a message like:
Access to fetch at 'https://cdn.example.com/master.m3u8' from origin 'https://your-site.example' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
You can also check the response headers directly. A working playlist response should include:
curl -I https://cdn.example.com/master.m3u8 HTTP/2 200 content-type: application/vnd.apple.mpegurl access-control-allow-origin: * access-control-allow-methods: GET, HEAD, OPTIONS access-control-allow-headers: Range, Origin
If the header is missing, or if the playlist passes but the .ts/.m4s segments fail, the same header must be added to the segment responses too. Each file type is a separate request and each needs its own CORS policy.
For nginx, add the headers inside the location block that serves HLS files:
location ~ \.(m3u8|ts|m4s|mpd)$ {
add_header Access-Control-Allow-Origin * always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Allow-Headers "Range, Origin" always;
add_header Access-Control-Expose-Headers "Content-Length, Content-Range" always;
types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; }
if ($request_method = OPTIONS) { return 204; }
}
On Amazon S3 plus CloudFront, configure a CORS policy on the bucket and make sure the CloudFront distribution forwards the Origin header and caches based on it. A cached response that was generated for a different origin is a common cause of intermittent failures.
On Cloudflare, a Transform Rule or a Worker can add the header at the edge:
export default {
async fetch(request) {
const response = await fetch(request);
const out = new Response(response.body, response);
out.headers.set("Access-Control-Allow-Origin", "*");
return out;
}
};
Restrict * to your own domain in production if the stream is private. Note that CORS is a browser-enforced rule, not authentication: it does not protect content from being fetched by non-browser clients.
.key) and subtitle files all need it as well.text/plain can be rejected before CORS is even evaluated. Use application/vnd.apple.mpegurl.xhrSetup or adding crossOrigin to the video element cannot add a missing response header. A same-origin proxy is the only client-side workaround, and it moves bandwidth costs onto your own server.curl -I on that URL and confirm access-control-allow-origin.