When hls.js reports MANIFEST_LOAD_ERROR and the browser console mentions a blocked cross-origin request, the fix is almost always on the server that hosts the stream, not in your player code. Every resource hls.js fetches with JavaScript - the master playlist, variant playlists, segments, and any AES-128 key - must be served with Access-Control-Allow-Origin. A CORS header on the master playlist alone is not enough.
How it works
Browsers apply the same-origin policy to JavaScript network reads. A <video src="..."> tag may load a cross-origin resource, but hls.js uses XHR or fetch to read the playlist text and the segment bytes into a MediaSource buffer, so those responses must explicitly grant permission to the page's origin.
Three details are easy to miss. First, the header is required on all responses in the chain: a 302 redirect, the master playlist, every variant, every .ts or .m4s segment, and #EXT-X-KEY key files. Second, hls.js sets a Range header for some requests, which turns a simple request into a preflighted one; the server must answer OPTIONS and allow Range in Access-Control-Allow-Headers. Third, Access-Control-Expose-Headers controls which response headers JavaScript may read, so Content-Length and Content-Range should be exposed when your logic uses byte ranges.
You can verify a header from the command line without opening a browser:
curl -s -D - -o /dev/null \
-H "Origin: https://your-page.example" \
https://cdn.example.com/vod/master.m3u8 | grep -i "access-control\|content-type\|HTTP/"
If no access-control-allow-origin line appears, the browser will block the read regardless of what your player does.
Step-by-step: configuration that works
1. Serve the whole HLS tree with consistent headers. A minimal nginx location block:
location ~* \.(m3u8|ts|m4s|key)$ {
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
video/iso.segment m4s;
application/octet-stream key;
}
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, Accept, X-Requested-With" always;
add_header Access-Control-Expose-Headers "Content-Length, Content-Range, Accept-Ranges" always;
add_header Access-Control-Max-Age 86400 always;
if ($request_method = OPTIONS) {
return 204;
}
try_files $uri =404;
}
The always keyword matters: without it, nginx omits add_header on error responses, so a 404 segment returns no CORS header and the browser reports a confusing cross-origin error instead of the real 404. Put this block where both playlists and segments are served; if variants live on a different host or bucket, configure that host too.
2. Handle cookie-based streams correctly. A wildcard origin cannot be combined with cookie-based authentication. In that case echo the requesting origin back and enable the matching browser option on the player side:
map $http_origin $cors_origin {
default "";
"~^https://(app|staging)\.example\.com$" $http_origin;
}
# inside the location block:
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
Vary: Origin stops a CDN from caching a response that was generated for one origin and replaying it for another.
3. Attach error diagnostics in hls.js. Knowing which request failed tells you which host is missing the header:
const hls = new Hls({
enableWorker: true,
xhrSetup: (xhr, url) => { xhr.setRequestHeader('X-Player', 'hlsjs-qa'); }
});
hls.on(Hls.Events.ERROR, (_evt, data) => {
console.warn(data.type, data.details, data.response?.code, data.url);
if (!data.fatal) return;
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();
else hls.destroy();
});
data.url is the exact resource that failed. If it is a segment or a key URI on another hostname, add the header there rather than retrying indefinitely on the player.
Common mistakes and parameters
| Error detail | Typical cause | Where to fix |
|---|---|---|
manifestLoadError | Master playlist has no CORS header | Playlist host |
manifestParsingError | Server returned HTML or a 403 page | Origin / auth rules |
fragLoadError | Segments or variants lack the header | Segment host or bucket |
keyLoadError | #EXT-X-KEY URI not permitted | Key endpoint |
status 0 in data.response | Preflight failed or request blocked | OPTIONS handling, allowed headers |
| Playlist works in Safari, fails in Chrome | Native HLS bypasses XHR | Add headers anyway for hls.js browsers |
| Header | Purpose | Common mistake |
|---|---|---|
Access-Control-Allow-Origin | Grants read access | Set only on the master playlist |
Access-Control-Allow-Headers | Permits Range and custom headers | Forgetting Range breaks preflight |
Access-Control-Expose-Headers | Lets JS read byte-range metadata | Missing Content-Range |
Vary: Origin | Keeps per-origin caches separate | CDN returns a stale header |
Troubleshooting
curl -Ishows the header but the browser still blocks: check the actual GET with anOriginheader, then inspect theOPTIONSpreflight; many servers only add the header to 200 responses.- A 404 or 403 segment still reports a CORS error: add
alwaysto everyadd_headerso error responses carry the header too. - Some viewers work and others do not: a proxy or CDN edge is caching an older response; purge the cache and confirm
Vary: Originis present. #EXT-X-KEYrequests fail while segments succeed: key files are fetched separately, often from a different path or host; include.keyin the same location rule.- The playlist loads but hls.js loops on
fragLoadError: stop retrying after a fixed number of attempts and logdata.url; endless retries hide the real 403 or timeout. - Everything works over HTTP but fails over HTTPS: mixed content is blocked before CORS is even evaluated; serve all resources over HTTPS.
Related reading
- Verify the playlist and segment headers with the stream inspector.
- Confirm the stream plays at all in the online HLS player before changing player code.
- Browse all HLS troubleshooting guides for buffering and playlist syntax issues.
- How to Play m3u8 in Chrome (3 Working Methods) - set up hls.js playback in Chrome.
- How to Test an HLS Stream Online - run a full playlist, segment and header check.
- Why Your HLS Stream Keeps Buffering - tune the bitrate ladder and buffer targets.