Zero-dependency HLS / MP4 media proxy with per-request header forwarding and .m3u8 manifest rewriting. Built on the Web Fetch API — drop it into a Next.js App Router route, a Cloudflare Worker, a Bun server, or a Hono adapter without changes.
When you embed a video stream that gates playback behind specific Referer / Origin / Cookie / User-Agent headers, the browser cannot send those itself — and CORS will block almost every direct request anyway. The usual fix is a server-side proxy, but writing one correctly is a surprising amount of trapdoors:
- HLS manifests reference segments by relative URL → each segment must also be rewritten back through the proxy.
- Variant playlists, EXT-X-KEY, EXT-X-MAP, EXT-X-MEDIA all carry
URI="..."attributes that need rewriting too. - The
Content-Encoding,Content-Length, andX-Frame-Optionsheaders from upstream must be stripped, or the browser will reject the response. - Range requests for MP4 must be forwarded, or seeking breaks.
- Headers the caller wants to forward must be opt-in, or you've built an open SSRF.
This package gets all of that right in ~150 lines and ships with TypeScript types.
npm install aetherly-stream-proxy// app/api/media-proxy/route.ts
import { createStreamProxyHandler } from 'aetherly-stream-proxy';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const { GET, HEAD, OPTIONS } = createStreamProxyHandler({
pathPrefix: '/api/media-proxy',
});That's the whole server. To build the URL your <video> or hls.js should fetch:
import { buildProxyUrl } from 'aetherly-stream-proxy';
const proxied = buildProxyUrl(
'https://example.cdn/stream/master.m3u8',
{ Referer: 'https://example.com/', 'User-Agent': 'Mozilla/5.0 ...' },
);
// → /api/media-proxy?url=...&h=eyJSZWZlcmVyIjoi...Point your player at that URL. The proxy fetches the manifest with your forwarded headers, rewrites every segment line so it also flows back through /api/media-proxy?h=..., and streams the bytes back to the browser.
import { createStreamProxyHandler } from 'aetherly-stream-proxy';
const proxy = createStreamProxyHandler();
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/media-proxy') {
if (request.method === 'OPTIONS') return proxy.OPTIONS();
if (request.method === 'HEAD') return proxy.HEAD(request);
return proxy.GET(request);
}
return new Response('Not found', { status: 404 });
},
};createStreamProxyHandler({
pathPrefix: '/api/media-proxy', // where this proxy is mounted
allowedHeaderPrefixes: ['referer', ...], // which client-supplied headers may flow upstream
defaultUserAgent: 'Mozilla/5.0 ...', // fallback UA
fetchImpl: fetch, // override for testing / custom agents
});- The
h=parameter is client-controlled. Header forwarding is gated byallowedHeaderPrefixes— by defaultreferer,origin,user-agent,cookie, and anyx-*. Anything else is dropped. - The proxy will follow redirects (
redirect: 'follow'). If you need to restrict the destinations, wrapfetchImpland validate the resolved URL. - This proxy intentionally has no host allowlist — pair it with
aetherly-embed-guard(or your own check) if you only want a fixed set of upstreams.
This library is provided as infrastructure for proxying HLS / MP4 media streams and forwarding HTTP headers. Users are responsible for ensuring they have the right to access, proxy, and redistribute the content they target. The authors do not endorse any specific use case and make no representation about the legality of upstream sources.
MIT