- Blog Posts
- Coming Soon
Coming Soon
Complete M3U8 CORS Error Solutions: The Ultimate Guide to Cross-Origin Issues
CORS (Cross-Origin Resource Sharing) errors are among the most common issues in M3U8 video playback. When you see errors like "Access to XMLHttpRequest has been blocked by CORS policy", don't panic. This article provides comprehensive solutions.
Understanding CORS Errors
What is CORS?
CORS is a browser security mechanism that controls how web pages from one domain can access resources from another domain. When your website tries to load M3U8 videos from a different domain, it may trigger CORS restrictions.
CORS Error Manifestations
// Common CORS error messages
Access to XMLHttpRequest at 'https://cdn.example.com/video.m3u8'
from origin 'https://yoursite.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
// Or
Cross-Origin Read Blocking (CORB) blocked cross-origin response
https://cdn.example.com/video.m3u8 with MIME type application/vnd.apple.mpegurl.
Root Causes of CORS Errors
graph LR
A[Your Website yoursite.com] -->|Request Video| B[CDN Server cdn.example.com]
B -->|Missing CORS Headers| C[Browser Blocks]
C -->|CORS Error| D[Video Cannot Play]
Quick CORS Problem Diagnosis
Using Browser Developer Tools
// Check CORS issues in console
fetch('https://cdn.example.com/video.m3u8')
.then(response => {
console.log('CORS Headers:', response.headers.get('Access-Control-Allow-Origin'));
console.log('Content-Type:', response.headers.get('Content-Type'));
})
.catch(error => {
console.error('CORS Error:', error);
});
Using curl Commands
# Check response headers
curl -I https://cdn.example.com/video.m3u8
# Check CORS-related headers
curl -H "Origin: https://yoursite.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: X-Requested-With" \
-X OPTIONS \
https://cdn.example.com/video.m3u8 -I
Solution 1: Server-Side Configuration
1. Nginx Configuration
# Add to nginx.conf or site configuration file
location ~* \.(m3u8|ts)$ {
# Allow all domains (use cautiously in production)
add_header Access-Control-Allow-Origin *;
# Or specify specific domain
add_header Access-Control-Allow-Origin https://yoursite.com;
# Allowed request methods
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
# Allowed request headers
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
# Allow credentials
add_header Access-Control-Allow-Credentials true;
# Preflight request cache time
add_header Access-Control-Max-Age 1728000;
# Handle OPTIONS preflight requests
if ($request_method = 'OPTIONS') {
return 204;
}
}
2. Apache Configuration
# Add to .htaccess or httpd.conf
<FilesMatch "\.(m3u8|ts)$">
# Allow cross-origin
Header set Access-Control-Allow-Origin "*"
# Or specify domain
Header set Access-Control-Allow-Origin "https://yoursite.com"
# Other CORS headers
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header set Access-Control-Allow-Headers "Origin, Content-Type, Accept, Range"
Header set Access-Control-Allow-Credentials "true"
</FilesMatch>
# Handle preflight requests
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
3. Node.js/Express Configuration
const express = require('express');
const cors = require('cors');
const app = express();
// Method 1: Using cors middleware
app.use(cors({
origin: 'https://yoursite.com', // Or use '*' to allow all
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Range'],
exposedHeaders: ['Content-Length', 'Content-Range']
}));
// Method 2: Manual CORS headers
app.use((req, res, next) => {
// Dynamically set allowed origins
const allowedOrigins = [
'https://yoursite.com',
'https://app.yoursite.com',
'http://localhost:3000'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Range');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');
if (req.method === 'OPTIONS') {
res.sendStatus(204);
} else {
next();
}
});
// Serve M3U8 files
app.get('/videos/:id.m3u8', (req, res) => {
res.type('application/vnd.apple.mpegurl');
res.sendFile(path.join(__dirname, 'videos', `${req.params.id}.m3u8`));
});
Solution 2: Using Proxy Server
1. Create Node.js Proxy
const express = require('express');
const axios = require('axios');
const app = express();
// CORS proxy endpoint
app.get('/proxy/m3u8', async (req, res) => {
const targetUrl = req.query.url;
if (!targetUrl) {
return res.status(400).json({ error: 'URL parameter required' });
}
try {
// Get M3U8 content
const response = await axios.get(targetUrl, {
responseType: 'text',
headers: {
'User-Agent': req.headers['user-agent'],
'Referer': req.headers.referer || targetUrl
}
});
// Set CORS headers
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
res.header('Content-Type', 'application/vnd.apple.mpegurl');
// Process M3U8 content, convert relative paths to absolute
let content = response.data;
const baseUrl = new URL(targetUrl).origin;
// Replace relative paths
content = content.replace(/^(?!#)(.+\.ts)$/gm, (match, p1) => {
if (p1.startsWith('http')) return p1;
return `${baseUrl}/${p1}`;
});
res.send(content);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Proxy TS segments
app.get('/proxy/ts', async (req, res) => {
const targetUrl = req.query.url;
try {
const response = await axios.get(targetUrl, {
responseType: 'stream'
});
res.header('Access-Control-Allow-Origin', '*');
res.header('Content-Type', 'video/MP2T');
response.data.pipe(res);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log('CORS proxy server running on port 3000');
});
2. Using Cloudflare Workers
// Cloudflare Workers script
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const targetUrl = url.searchParams.get('url');
if (!targetUrl) {
return new Response('Missing URL parameter', { status: 400 });
}
// Fetch target resource
const response = await fetch(targetUrl, {
headers: {
'User-Agent': request.headers.get('User-Agent'),
'Referer': request.headers.get('Referer')
}
});
// Create new response with CORS headers
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set('Access-Control-Allow-Origin', '*');
modifiedResponse.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
modifiedResponse.headers.set('Cache-Control', 'public, max-age=3600');
// Process M3U8 content
if (targetUrl.includes('.m3u8')) {
const text = await response.text();
const baseUrl = new URL(targetUrl).origin;
// Convert relative URLs to absolute
const modifiedText = text.replace(/^(?!#)(.+\.ts)$/gm, (match, p1) => {
if (p1.startsWith('http')) return p1;
return `${url.origin}/?url=${encodeURIComponent(baseUrl + '/' + p1)}`;
});
return new Response(modifiedText, {
headers: modifiedResponse.headers
});
}
return modifiedResponse;
}
Solution 3: Frontend Handling
1. Using HLS.js Configuration
import Hls from 'hls.js';
// Configure HLS.js to handle CORS
const config = {
// Enable CORS credentials
xhrSetup: function(xhr, url) {
xhr.withCredentials = false; // If cookies not needed
// Add custom headers
xhr.setRequestHeader('X-Custom-Header', 'value');
},
// Use fetch instead of XHR
loader: class CustomLoader extends Hls.DefaultConfig.loader {
load(context, config, callbacks) {
const { url, responseType } = context;
fetch(url, {
method: 'GET',
mode: 'cors', // Explicitly specify CORS mode
credentials: 'omit', // Don't send cookies
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return responseType === 'arraybuffer'
? response.arrayBuffer()
: response.text();
})
.then(data => {
callbacks.onSuccess({
url,
data
}, { }, context);
})
.catch(error => {
callbacks.onError({
code: 0,
text: error.message
}, context);
});
}
}
};
const hls = new Hls(config);
2. Using Video.js
import videojs from 'video.js';
import 'videojs-contrib-hls';
// Configure Video.js
const player = videojs('my-video', {
html5: {
hls: {
withCredentials: false,
// Override XHR
beforeRequest: function(options) {
// Modify request options
options.headers = options.headers || {};
options.headers['X-Custom-Header'] = 'value';
// Use proxy URL
if (options.uri.includes('example.com')) {
options.uri = `/proxy?url=${encodeURIComponent(options.uri)}`;
}
return options;
}
}
}
});
3. Create Universal CORS Handler
class CORSHandler {
constructor(proxyUrl = '/proxy') {
this.proxyUrl = proxyUrl;
}
// Detect CORS support
async checkCORS(url) {
try {
const response = await fetch(url, {
method: 'HEAD',
mode: 'cors'
});
return response.ok;
} catch (error) {
console.log('CORS not supported for:', url);
return false;
}
}
// Get proxy URL
getProxyUrl(originalUrl) {
return `${this.proxyUrl}?url=${encodeURIComponent(originalUrl)}`;
}
// Process M3U8 content
async processM3U8(url) {
const supportsCORS = await this.checkCORS(url);
const targetUrl = supportsCORS ? url : this.getProxyUrl(url);
const response = await fetch(targetUrl);
let content = await response.text();
if (!supportsCORS) {
// Process segment URLs in M3U8
const baseUrl = new URL(url).origin;
content = content.split('\n').map(line => {
if (line.endsWith('.ts') && !line.startsWith('http')) {
const tsUrl = `${baseUrl}/${line}`;
return this.getProxyUrl(tsUrl);
}
return line;
}).join('\n');
}
return content;
}
// Create Blob URL
createBlobUrl(content) {
const blob = new Blob([content], {
type: 'application/vnd.apple.mpegurl'
});
return URL.createObjectURL(blob);
}
}
// Usage example
const corsHandler = new CORSHandler();
async function playM3U8(videoUrl) {
try {
const content = await corsHandler.processM3U8(videoUrl);
const blobUrl = corsHandler.createBlobUrl(content);
// Play using blob URL
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(blobUrl);
hls.attachMedia(document.getElementById('video'));
}
} catch (error) {
console.error('Failed to play video:', error);
}
}
Solution 4: CDN Configuration
1. AWS CloudFront Configuration
# CloudFront behavior configuration
Behaviors:
- PathPattern: "*.m3u8"
TargetOriginId: MyOrigin
ViewerProtocolPolicy: allow-all
ResponseHeadersPolicyId: !Ref ResponseHeadersPolicy
ResponseHeadersPolicy:
CorsConfig:
AccessControlAllowOrigins:
Items: ["*"]
AccessControlAllowMethods:
Items: ["GET", "HEAD", "OPTIONS"]
AccessControlAllowHeaders:
Items: ["*"]
AccessControlExposeHeaders:
Items: ["Content-Length", "Content-Range"]
AccessControlMaxAgeSec: 86400
2. Fastly Configuration
# Fastly VCL configuration
sub vcl_recv {
if (req.url ~ "\.(m3u8|ts)$") {
set req.http.Access-Control-Allow-Origin = "*";
}
}
sub vcl_deliver {
if (req.url ~ "\.(m3u8|ts)$") {
set resp.http.Access-Control-Allow-Origin = "*";
set resp.http.Access-Control-Allow-Methods = "GET, OPTIONS";
set resp.http.Access-Control-Allow-Headers = "Content-Type, Range";
}
}
3. Cloudflare Configuration
// Page Rules or Transform Rules
// Add response header rules
URL Pattern: *example.com/*.m3u8
Add Response Header: Access-Control-Allow-Origin = *
Solution 5: Using Service Worker
// sw.js - Service Worker file
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Intercept M3U8 and TS requests
if (url.pathname.endsWith('.m3u8') || url.pathname.endsWith('.ts')) {
event.respondWith(
fetch(event.request.url, {
mode: 'no-cors' // Bypass CORS check
})
.then(response => {
// Create new response with CORS headers
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('Content-Type',
url.pathname.endsWith('.m3u8')
? 'application/vnd.apple.mpegurl'
: 'video/MP2T'
);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: headers
});
})
.catch(error => {
console.error('Service Worker fetch error:', error);
return new Response('Error fetching resource', { status: 500 });
})
);
}
});
// Register Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.error('SW registration failed:', error));
}
Debugging Tips
1. Chrome Developer Tools
// Disable CORS check (for development only)
// Windows
chrome.exe --user-data-dir="C:/Chrome dev session" --disable-web-security
// macOS
open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
// Linux
google-chrome --disable-web-security --user-data-dir="/tmp/chrome_dev_test"
2. Using Local Proxy Tools
# Using local-cors-proxy
npm install -g local-cors-proxy
lcp --proxyUrl https://example.com --port 8010
# Using cors-anywhere
npm install cors-anywhere
node server.js
3. Test Script
<!DOCTYPE html>
<html>
<head>
<title>CORS Test</title>
</head>
<body>
<h1>M3U8 CORS Test</h1>
<div id="result"></div>
<script>
async function testCORS(url) {
const resultDiv = document.getElementById('result');
try {
// Test 1: Simple request
const response1 = await fetch(url, { mode: 'cors' });
resultDiv.innerHTML += `✅ Simple request succeeded<br>`;
// Test 2: Preflight request
const response2 = await fetch(url, {
mode: 'cors',
method: 'OPTIONS'
});
resultDiv.innerHTML += `✅ Preflight request succeeded<br>`;
// Check response headers
const headers = {};
response1.headers.forEach((value, key) => {
if (key.toLowerCase().includes('access-control')) {
headers[key] = value;
}
});
resultDiv.innerHTML += `<pre>${JSON.stringify(headers, null, 2)}</pre>`;
} catch (error) {
resultDiv.innerHTML += `❌ CORS Error: ${error.message}<br>`;
}
}
// Test your M3U8 URL
testCORS('https://example.com/video.m3u8');
</script>
</body>
</html>
Best Practices
1. Secure CORS Configuration
// Don't do this (too permissive)
res.header('Access-Control-Allow-Origin', '*');
// Recommended approach (whitelist control)
const allowedOrigins = [
'https://yoursite.com',
'https://app.yoursite.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
2. Cache CORS Preflight
# Nginx configuration optimization
location ~* \.(m3u8|ts)$ {
# Cache preflight requests for 24 hours
add_header Access-Control-Max-Age 86400;
# Only return CORS headers when needed
if ($http_origin ~* (https?://.*\.yoursite\.com)) {
add_header Access-Control-Allow-Origin $http_origin;
}
}
3. Monitor CORS Errors
// Frontend error monitoring
window.addEventListener('unhandledrejection', event => {
if (event.reason && event.reason.message &&
event.reason.message.includes('CORS')) {
// Report CORS error
console.error('CORS Error detected:', {
url: event.reason.url,
message: event.reason.message,
timestamp: new Date().toISOString()
});
// Send to monitoring service
fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({
type: 'CORS',
error: event.reason.message
})
});
}
});
Conclusion
While CORS errors are common, they can be completely resolved with proper configuration and handling strategies. Key points:
- Understand CORS Mechanism - Know why CORS errors occur
- Choose Appropriate Solution - Select server-side, CDN, or frontend solutions based on control permissions
- Security First - Don't blindly use
Access-Control-Allow-Origin: *
- Performance Optimization - Use caching to reduce preflight requests
- Backup Plan - Prepare proxy service as fallback
If you don't want to deal with complex CORS configurations, use M3U8 Player online player, which has already handled common CORS issues, letting you focus on the video content itself.