From 450c5def97864661fcdcea8c7a58e75869ae9885 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 8 Sep 2026 17:19:32 +0200 Subject: [PATCH] Fix incorrect use of Content-Length when the server compresses the content (fixes #487) --- src/source.ts | 8 +++++++- test/node/url-source.test.ts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/source.ts b/src/source.ts index 0c57015..3edea96 100644 --- a/src/source.ts +++ b/src/source.ts @@ -955,7 +955,13 @@ export class UrlSource extends PathedSource { } outer: - if (this._orchestrator.fileSize === null) { + if ( + this._orchestrator.fileSize === null + // Content-Range/Length fields are meaningless if Content-Encoding is present. Content-Encoding is + // basically never used for range responses (since the encoding runs *before* the slicing), so we're set + // in that case. + && (response.status === 206 || (response.type === 'basic' && !response.headers.has('Content-Encoding'))) + ) { // See if we can deduce the file size from the response const contentRange = response.headers.get('Content-Range'); diff --git a/test/node/url-source.test.ts b/test/node/url-source.test.ts index 092dd55..ed9acf6 100644 --- a/test/node/url-source.test.ts +++ b/test/node/url-source.test.ts @@ -2,6 +2,9 @@ import { expect, test } from 'vitest'; import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; +import { brotliCompressSync } from 'node:zlib'; +import { assert } from '../../src/misc.js'; +import { Reader, readBytes } from '../../src/reader.js'; import { ALL_FORMATS, EncodedPacket, @@ -225,6 +228,39 @@ test('UrlSource in sequential mode downloads lazily and aborts the response on d } }, 10_000); +test('UrlSource reads the full decoded body of a compressed response', async () => { + const text = 'Some playlist text\n'.repeat(256); + const content = Buffer.from(text); + const compressed = brotliCompressSync(content); + const server = http.createServer((req, res) => { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Encoding': 'br', + 'Content-Length': compressed.byteLength, + }); + res.end(compressed); + }); + + await new Promise(resolve => server.listen(0, resolve)); + + try { + const address = server.address(); + assert(address && typeof address !== 'string'); + const source = new UrlSource(`http://localhost:${address.port}/playlist.m3u8`); + using ref = source.ref(); + const reader = new Reader(ref.source); + + const slice = await reader.requestEntireFile(); + expect(slice).not.toBeNull(); + expect(compressed.byteLength).toBeLessThan(content.byteLength); + expect(slice!.length).toBe(content.byteLength); + expect(Buffer.from(readBytes(slice!, slice!.length)).toString()).toBe(text); + } finally { + server.closeAllConnections(); + server.close(); + } +}); + const startRangelessServer = async ( options: { responseByteLimits?: number[]; trailingPaddingSize?: number } = {}, ) => {