Stream Video Downloader logo
← Back to Blog

Why CORS Blocks Video Downloads (and How a Browser Extension Bypasses It)

July 6, 2026 · 6 min read

The video plays fine on the page you found it on. Open the browser console and try to fetch() that same .m3u8URL yourself, or hand it to a tool running on a different site, and it fails instantly with a CORS error. Nothing about the video changed — only who's asking for it did. That contradiction trips up almost everyone the first time they try to build or use a video downloader, and it's the exact problem this site's download flow is built around solving.

CORS is about who's reading, not what's being read

The same-origin policy is the browser rule that JavaScript running on origin-a.com can't read response bytes from origin-b.com unless origin-b.com explicitly opts in with an Access-Control-Allow-Origin header naming that origin (or *). CORS is the mechanism that relaxes this rule when a server chooses to. Critically, the restriction lives on the reading side, not the file itself — the exact same .m3u8URL can be perfectly fetchable from one origin and blocked from another, because the CDN's CORS header is a per-origin allowlist, not a public/private switch on the resource.

Why playback doesn't need permission but fetching does

A plain <video src="..."> — or Safari loading a .m3u8 natively, as covered in why Chrome can't play HLS natively — never hands raw bytes to JavaScript at all. The browser's media stack requests the data and pipes it straight to the decoder; nothing about that path is subject to CORS, which is why a page can play video from a CDN that sends no CORS headers whatsoever. The moment something needs to read those bytes in JavaScript instead of just rendering them — which is exactly what a fetch()-based HLS player like hls.js does for every segment before handing decoded-ready data to MSE's appendBuffer — the request goes through the normal fetch/XHR pipeline and CORS applies. That's also why a site playing its own HLS stream via hls.js usually already needs its CDN to allow CORS for its own origin; the site's player wouldn't work otherwise. What it doesn't need is to allow every other origin — and it almost never does, because there's no reason for a video CDN to permit arbitrary third-party sites to read its segment bytes.

Why a downloader hits this even harder

A tool that detects a stream on one page and then tries to fetch it from a completely different origin — a downloader website, a different tab, anything that isn't the original page — is asking for exactly the cross-origin read the CDN's allowlist wasn't written to permit. It doesn't matter that a human, watching in their own browser, is clearly allowed to see this video; CORS has no concept of user intent, only origin headers. Range-request byte fetching (used to grab specific segments efficiently) and any custom headers on top of that can also trigger a CORS preflight OPTIONS request, adding a second point where a permissive CDN can still reject the follow-up.

How a browser extension actually gets around this

The fix isn't a trick against CORS — it's a different actor making the request. A Chrome extension's background service worker, granted host_permissionsfor the sites it needs (this project's bridge-bg.js uses <all_urls>), can issue fetch()calls from the extension platform itself rather than from a web page's script context. Those calls aren't subject to the page-level same-origin policy at all: the browser treats a permission the user granted at install time as sufficient authorization, so the target server's CORS allowlist — which was never written with this extension in mind — simply doesn't come into play. This is a deliberate, documented capability of the extension platform, not a bug being exploited.

That's why the actual network fetch in this site's architecture never happens in the website's own page script. The website (running at its own origin) can't call chrome.* APIs directly, so it posts a message to bridge.js— a content script the extension injects only on this site's origin — which relays the request over chrome.runtime messaging to the background worker. bridge-bg.jsperforms the real fetch with its elevated permissions and sends the bytes back along the same path. From the website's point of view, that whole round trip is just fetchUrl(url) resolving to a Uint8Array — the CORS-bypassing part is entirely on the other side of a message-passing tunnel, never a direct cross-origin call from the page itself.

The safeguard that comes with that power

Fetching without CORS restrictions from a background worker with <all_urls> permissions is powerful enough that it needs its own guardrail: bridge-bg.js runs every requested URL through an isSafeUrl() check before fetching, rejecting localhost, 127.x, 10.x, 192.168.x, and 172.16–31.x addresses. Bypassing CORS is one thing; letting a compromised or malicious page use that same channel to probe a user's private network is a completely different, unacceptable outcome, so the bridge only ever forwards requests aimed at public, external stream URLs — exactly the kind of resource it was built to fetch.

What happens without the extension

The website still works without it, via the fallback in the bridge-first fetch pattern used across the player and the converter: try fetchUrl() through the extension first, and if that throws — extension not installed, or the handshake times out — fall back to a plain fetch(url, { credentials: "omit" })from the page itself. That plain fetch only succeeds if the CDN happens to allow CORS for arbitrary origins, which is uncommon. In practice, that means most protected streams simply won't download without the extension installed — which is also the core of why this tool needs no server to stay free and unlimited: the privileged fetch happens in the browser the user already controls, not on infrastructure someone else has to run and pay for.

FAQ

If the video plays on the page, why can't I just save it normally? Because playback and reading raw bytes are different operations to the browser. The <video> element (or a same-origin player) is allowed to render the stream without CORS; a separate tool trying to read and reassemble the same bytes is making a cross-origin read the CDN never authorized.

Could a server-side proxy solve this instead?Yes — a backend that fetches the stream and hands it to the browser sidesteps CORS entirely, since the browser only ever talks to its own server. That reintroduces the exact thing this project avoids: a backend to run, scale, and pay for, plus somewhere the video data passes through that isn't the user's own machine.

Does this mean the extension can read anything on any site? Its host_permissions allow it to, which is why installing it is a real trust decision — but the bridge itself only forwards deliberate fetch requests for stream URLs the page explicitly asks for, and the isSafeUrl() check blocks the private-network address ranges regardless of what asked.