HTTP 416: erschoepftes Range-Konflikt-Item macht bounded Clean-Restart statt dauerhaft zu haengen
Nutzer-Report (ultracode-Diagnose, 6-Agenten): ein Item mit "Fehler: HTTP 416"
(Range Not Satisfiable) wurde gar nicht mehr erneut versucht und blockierte das
ganze Paket ("Entpacken - Ausstehend" bei allen fertigen Geschwistern). Befund
code-belegt: nach dem In-Run-416-Budget (maxHttp416Retries) lief das Item in
einen Endzustand:
- retryLimit>0 (bare "HTTP 416", directLinkRetryMatch=false): direkt terminal
"failed" (download-manager ~9442). Der einzige Re-Retry-Pfad
recoverRetryableItems() laeuft NUR bei Boot/Start, nie mid-run -> mit
autoReconnect=false bleibt es bis zum Neustart "failed". Und da jeder
Extract-Zweig auf failed===0 gated, friert EIN failed-Item das ganze Paket
ein -> exakt das Screenshot-Symptom.
- retryLimit=0 (wrapped "direct_link_retry_exhausted:HTTP 416"): faellt nach dem
416-Budget in den generischen Retry (9349) mit maxGenericErrorRetries=
MAX_SAFE_INTEGER -> unbegrenzte futile Re-Unrestricts (haemmert den Hoster,
Item kommt nie weiter).
Fix: bei erschoepftem 416-Budget an BEIDEN Stellen (gewrappt @9320, bare @9442)
nicht mehr terminalisieren bzw. endlos generisch retrien, sondern via neuem
escalateHttp416OrFail() einen kompletten Neu-Download von 0 anstossen: Partial
loeschen (rmSync), Bytes/Progress/Budget zuruecksetzen, mit Delay re-queuen. Da
416 ein abgelehnter Resume-Range ist, ist der korrekte Weg ein frischer
Download ohne Range-Header (kein 416 mehr) statt erneuter Resume. Hart gedeckelt
ueber http416FreshRestartByItem (MAX_HTTP416_FRESH_RESTARTS=2, retryLimit-
unabhaengig) -> ein wirklich toter Link faellt nach begrenzten sauberen
Neustarts terminal, kein Endlos-Hammering. Zaehler wird in start() und beim
terminalen Fail geleert. Fresh-Restart-Delay via RD_HTTP416_FRESH_RESTART_DELAY_MS
testbar.
Sobald das Item den failed-Zustand verlaesst, entsperrt die bestehende
failed===0-Logik das Paket-Entpacken von selbst. (Die optionale Haertung
"fertige Archiv-Sets trotz failed-Geschwister entpacken" + "Voll-Datei-416 als
fertig werten statt neu laden" sind bewusst als Folgeschritt offen.)
Tests: Recovery (416 erschoepft -> Clean-Restart -> Item completed, retryLimit>0
bare) + Bounded-Fail (persistentes 416 -> begrenzte Neustarts -> failed,
retryLimit=0 wrapped, terminiert statt Endlos). Suite 945 gruen, tsc=6.
This commit is contained in:
@@ -2209,6 +2209,98 @@ describe("download manager", () => {
|
||||
expect(fs.statSync(item.targetPath).size).toBe(binary.length);
|
||||
});
|
||||
|
||||
it("recovers an HTTP 416 item with a clean fresh restart after the in-budget retries are exhausted", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-416-fresh-"));
|
||||
tempDirs.push(root);
|
||||
const binary = Buffer.alloc(160 * 1024, 19);
|
||||
const prevDelay = process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS;
|
||||
process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS = "0";
|
||||
let downloadCalls = 0;
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("/unrestrict/link")) {
|
||||
return new Response(JSON.stringify({ download: "https://dummy/direct-416-recover", filename: "fresh-416.mkv", filesize: binary.length }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
};
|
||||
|
||||
try {
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract"), retryLimit: 2, autoExtract: false, autoReconnect: false },
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
(manager as any).downloadToFile = async (_active: unknown, _directUrl: string, targetPath: string) => {
|
||||
downloadCalls += 1;
|
||||
if (downloadCalls <= 3) {
|
||||
throw new Error("HTTP 416");
|
||||
}
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, binary);
|
||||
const item = Object.values((manager as any).session.items)[0] as { downloadedBytes: number; totalBytes: number; progressPercent: number } | undefined;
|
||||
if (item) {
|
||||
item.downloadedBytes = binary.length;
|
||||
item.totalBytes = binary.length;
|
||||
item.progressPercent = 100;
|
||||
}
|
||||
return { resumable: true };
|
||||
};
|
||||
|
||||
manager.addPackages([{ name: "fresh-416", links: ["https://dummy/fresh-416"] }]);
|
||||
await manager.start();
|
||||
await waitFor(() => !manager.getSnapshot().session.running, 20000);
|
||||
|
||||
const item = Object.values(manager.getSnapshot().session.items)[0];
|
||||
expect(item?.status).toBe("completed");
|
||||
expect(downloadCalls).toBeGreaterThan(3);
|
||||
} finally {
|
||||
if (prevDelay === undefined) { delete process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS; } else { process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS = prevDelay; }
|
||||
}
|
||||
}, 25000);
|
||||
|
||||
it("bounds HTTP 416 clean restarts and finally fails instead of looping forever or stalling permanently", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-416-cap-"));
|
||||
tempDirs.push(root);
|
||||
const prevDelay = process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS;
|
||||
process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS = "0";
|
||||
let downloadCalls = 0;
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("/unrestrict/link")) {
|
||||
return new Response(JSON.stringify({ download: "https://dummy/direct-416-forever", filename: "always-416.mkv", filesize: 1024 }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
throw new Error(`unexpected fetch ${url}`);
|
||||
};
|
||||
|
||||
try {
|
||||
const manager = new DownloadManager(
|
||||
{ ...defaultSettings(), token: "rd-token", outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract"), retryLimit: 0, autoExtract: false, autoReconnect: false },
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
(manager as any).downloadToFile = async () => {
|
||||
downloadCalls += 1;
|
||||
throw new Error("direct_link_retry_exhausted:HTTP 416");
|
||||
};
|
||||
|
||||
manager.addPackages([{ name: "always-416", links: ["https://dummy/always-416"] }]);
|
||||
await manager.start();
|
||||
await waitFor(() => !manager.getSnapshot().session.running, 20000);
|
||||
|
||||
const item = Object.values(manager.getSnapshot().session.items)[0];
|
||||
expect(item?.status).toBe("failed");
|
||||
expect(downloadCalls).toBeGreaterThan(4);
|
||||
expect(downloadCalls).toBeLessThan(30);
|
||||
expect((manager as any).http416FreshRestartByItem.get(item.id)).toBeUndefined();
|
||||
} finally {
|
||||
if (prevDelay === undefined) { delete process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS; } else { process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS = prevDelay; }
|
||||
}
|
||||
}, 25000);
|
||||
|
||||
it("retries HTTP 416 in-session when using Debrid-Link API and then completes", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Reference in New Issue
Block a user