▶M3U8 SPACE

How to Fix hls.js CORS Errors

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 -

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 detailTypical causeWhere to fix
manifestLoadErrorMaster playlist has no CORS headerPlaylist host
manifestParsingErrorServer returned HTML or a 403 pageOrigin / auth rules
fragLoadErrorSegments or variants lack the headerSegment host or bucket
keyLoadError#EXT-X-KEY URI not permittedKey endpoint
status 0 in data.responsePreflight failed or request blockedOPTIONS handling, allowed headers
Playlist works in Safari, fails in ChromeNative HLS bypasses XHRAdd headers anyway for hls.js browsers
HeaderPurposeCommon mistake
Access-Control-Allow-OriginGrants read accessSet only on the master playlist
Access-Control-Allow-HeadersPermits Range and custom headersForgetting Range breaks preflight
Access-Control-Expose-HeadersLets JS read byte-range metadataMissing Content-Range
Vary: OriginKeeps per-origin caches separateCDN returns a stale header

Troubleshooting

  • curl -I shows the header but the browser still blocks: check the actual GET with an Origin header, then inspect the OPTIONS preflight; many servers only add the header to 200 responses.
  • A 404 or 403 segment still reports a CORS error: add always to every add_header so 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: Origin is present.
  • #EXT-X-KEY requests fail while segments succeed: key files are fetched separately, often from a different path or host; include .key in the same location rule.
  • The playlist loads but hls.js loops on fragLoadError: stop retrying after a fixed number of attempts and log data.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