fix(anime): never forward Range to the bridge so 206 replies cannot skip the strip

ffmpeg opens every HLS segment with 'Range: bytes=0-'. The bridge
answers some of those 206 (depending on its cache state), and the proxy
only rewrites full 200 bodies, so the PNG disguise passed through
untouched and whether an episode played was a coin flip: first attempts
died with "no audio or video data played", the same URL played fine
once the bridge had the segments cached.

Reproduced end to end against a private bridge + mpv driven with the
app's exact commands; with Range dropped every segment is stripped and
playback runs.
This commit is contained in:
2026-08-01 00:40:34 -07:00
parent c2781a3f80
commit 1c50ff74cb
3 changed files with 42 additions and 0 deletions
@@ -156,6 +156,42 @@ test('proxy rewrites absolute upstream playlist entries to its own origin', asyn
}
});
test('proxy strips even when the client asks for a byte range', async () => {
// ffmpeg opens every HLS segment with `Range: bytes=0-`; the bridge answers
// some of those with 206, which must not bypass the strip.
const ts = makeTsPackets(8);
const disguised = Buffer.concat([PNG_HEADER, ts]);
const upstream = http.createServer((req, res) => {
if (req.headers.range !== undefined) {
res.writeHead(206, {
'content-type': 'image/png',
'content-range': `bytes 0-${disguised.length - 1}/${disguised.length}`,
});
res.end(disguised);
return;
}
res.writeHead(200, { 'content-type': 'image/png' });
res.end(disguised);
});
await new Promise<void>((resolve) => upstream.listen(0, '127.0.0.1', resolve));
const upstreamOrigin = `http://127.0.0.1:${(upstream.address() as AddressInfo).port}`;
const proxy = await startStreamStripProxy({
upstreamOrigin: () => upstreamOrigin,
retryDelayMs: 5,
});
try {
const response = await fetch(`${proxy.origin}/video/seg.ts`, {
headers: { Range: 'bytes=0-' },
});
const body = Buffer.from(await response.arrayBuffer());
assert.equal(response.status, 200);
assert.deepEqual(body, ts);
} finally {
await proxy.close();
await new Promise<void>((resolve) => upstream.close(() => resolve()));
}
});
test('proxy forwards error statuses without touching the body', async () => {
await withProxy(
{ '/video/gone.ts': { status: 404, contentType: 'text/plain', body: Buffer.from('nope') } },
+5
View File
@@ -119,6 +119,11 @@ export function startStreamStripProxy(
const requestHeaders = forwardableHeaders(req.headers);
delete requestHeaders.host;
// Never forward Range: ffmpeg opens every segment with `bytes=0-`, the
// bridge answers some of those 206, and a partial response cannot be
// stripped (only full 200 bodies are). Byte ranges into a resource whose
// bytes this proxy rewrites would be incoherent anyway.
delete requestHeaders.range;
res.on('error', () => {});
requestUpstream(req, res, upstreamUrl, requestHeaders, 0);