Blog
Engineering 3 min read

hls.js CORS error: why it happens and how to fix it

hls.js fetches the manifest and every segment over fetch/XHR, so both need CORS headers. Here's why the error happens and the exact headers that fix it.

rehelios

Engineering

The hls.js CORS error means the server delivering your .m3u8 playlist and its segments isn’t sending an Access-Control-Allow-Origin header that matches your page. hls.js loads video through fetch/XHR, so — unlike a native <video src> tag — every manifest and segment is subject to CORS. Fix it on the origin/CDN: send Access-Control-Allow-Origin, allow the Range header, and don’t redirect across origins mid-stream.

Why hls.js trips CORS when a plain video tag doesn’t

Point a Safari <video> element at an HLS URL and it plays with no CORS check — native playback is exempt. hls.js can’t do that on most browsers: it uses Media Source Extensions, which means it downloads the manifest and each segment itself via fetch/XHR, appends the bytes to a SourceBuffer, and drives playback in JavaScript. Anything JavaScript fetches cross-origin goes through CORS. So the moment your player and your video live on different origins — app.example.com loading segments from cdn.example.com — the browser demands the right headers.

What the error actually looks like

hls.js surfaces it as a NETWORK_ERROR — usually manifestLoadError (the .m3u8 failed) or fragLoadError (a segment failed) — while the browser console shows the real cause:

Access to fetch at 'https://cdn.example.com/out.m3u8' from origin
'https://app.example.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

The manifest and the segments are separate requests. It’s common for the .m3u8 to load fine and then every .ts/.m4s segment to fail — same fix, but the headers have to be on the segment responses too, not just the playlist.

The fix: headers on every file hls.js touches

On the origin / CDN

Send Access-Control-Allow-Origin on the manifest and every segment. Allow Range in Access-Control-Allow-Headers — hls.js uses byte-range requests, which trigger a preflight.

Avoid cross-origin redirects

A 302 from the manifest URL to a different host restarts the CORS check against the new origin. Serve segments from the same host the manifest points to.

Concretely, the responses need at least:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Range, Origin, Accept

A few things that bite people after the headers are set:

  • Credentials. If you set xhrSetup to send cookies (withCredentials = true), you can no longer use Access-Control-Allow-Origin: * — it must echo the exact origin, and you must add Access-Control-Allow-Credentials: true.
  • Signed URLs. If playback uses signed segment URLs, an expired token reads as a load error, not a CORS error — check the token before blaming CORS.
  • Wildcard vs exact. * is fine for public video without credentials; lock it to your origin once you send cookies or auth headers.

The one-line version

CORS lives on the server, not in hls.js — so no player config makes an origin without Access-Control-Allow-Origin work. Set the headers on the manifest and the segments, allow Range, keep everything on one host, and the error goes away.

This is exactly the kind of plumbing rehelios handles for you: manifests and segments are served from a global CDN with correct CORS headers and byte-range support by default, so hls.js — or a native player — just works. See how playback and access control are set up, and the concepts behind it in HLS vs DASH and what a video CDN does.

Put your first video live today

Create an account, upload a file, and have a fast, embeddable video live in minutes. Pay only for what you store and stream.