From ea6301d3260279dc26aaabe7ec2fa09518f3149b Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 21:43:40 +0100 Subject: [PATCH] Release v1.4.16 with crash prevention and hang protection - Add 30s fetch timeouts to ALL API calls (Real-Debrid, BestDebrid, AllDebrid, Mega-Web) - Fix race condition in concurrent worker indexing (runWithConcurrency) - Guard JSON.parse in RealDebrid response with try-catch - Add try-catch to fs.mkdirSync in download pipeline (handles permission denied) - Convert MD5/SHA1 hashing to streaming (prevents OOM on large files) - Add error handling for hash manifest file reading - Prevent infinite hangs on unresponsive API endpoints Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 4 ++-- package.json | 2 +- src/main/debrid.ts | 29 ++++++++++++++++++++++------- src/main/download-manager.ts | 6 +++++- src/main/integrity.ts | 16 ++++++++++++---- src/main/mega-web-fallback.ts | 12 ++++++++---- src/main/realdebrid.ts | 10 ++++++++-- 7 files changed, 58 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc0ddd1..d7fa06d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.4.15", + "version": "1.4.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.4.15", + "version": "1.4.16", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 8f74cbb..9c762a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.15", + "version": "1.4.16", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 20b9cbb..6fdf5f1 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -1,8 +1,11 @@ import { AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types"; import { REQUEST_RETRIES } from "./constants"; +import { logger } from "./logger"; import { RealDebridClient, UnrestrictedLink } from "./realdebrid"; import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils"; +const API_TIMEOUT_MS = 30000; + const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1"; const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4"; @@ -216,11 +219,19 @@ async function runWithConcurrency(items: T[], concurrency: number, worker: (i } const size = Math.max(1, Math.min(concurrency, items.length)); let index = 0; + const next = (): T | undefined => { + if (index >= items.length) { + return undefined; + } + const item = items[index]; + index += 1; + return item; + }; const runners = Array.from({ length: size }, async () => { - while (index < items.length) { - const current = items[index]; - index += 1; + let current = next(); + while (current !== undefined) { await worker(current); + current = next(); } }); await Promise.all(runners); @@ -243,7 +254,8 @@ async function resolveRapidgatorFilename(link: string): Promise { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9,de;q=0.8" - } + }, + signal: AbortSignal.timeout(API_TIMEOUT_MS) }); if (!response.ok) { if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES + 2) { @@ -348,7 +360,8 @@ class BestDebridClient { const response = await fetch(request.url, { method: "GET", - headers + headers, + signal: AbortSignal.timeout(API_TIMEOUT_MS) }); const text = await response.text(); const parsed = parseJson(text); @@ -432,7 +445,8 @@ class AllDebridClient { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "RD-Node-Downloader/1.1.15" }, - body + body, + signal: AbortSignal.timeout(API_TIMEOUT_MS) }); const text = await response.text(); @@ -484,7 +498,8 @@ class AllDebridClient { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "RD-Node-Downloader/1.1.12" }, - body: new URLSearchParams({ link }) + body: new URLSearchParams({ link }), + signal: AbortSignal.timeout(API_TIMEOUT_MS) }); const text = await response.text(); const payload = asRecord(parseJson(text)); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 1e5e3aa..319c680 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1851,7 +1851,11 @@ export class DownloadManager extends EventEmitter { item.provider = unrestricted.provider; item.retries += unrestricted.retriesUsed; item.fileName = sanitizeFilename(unrestricted.fileName || filenameFromUrl(item.url)); - fs.mkdirSync(pkg.outputDir, { recursive: true }); + try { + fs.mkdirSync(pkg.outputDir, { recursive: true }); + } catch (mkdirError) { + throw new Error(`Zielordner kann nicht erstellt werden: ${compactErrorText(mkdirError)}`); + } const existingTargetPath = String(item.targetPath || "").trim(); const canReuseExistingTarget = existingTargetPath && isPathInsideDir(existingTargetPath, pkg.outputDir) diff --git a/src/main/integrity.ts b/src/main/integrity.ts index 9c5a849..5489050 100644 --- a/src/main/integrity.ts +++ b/src/main/integrity.ts @@ -50,7 +50,12 @@ export function readHashManifest(packageDir: string): Map((resolve, reject) => { + stream.on("data", (chunk: string | Buffer) => hash.update(typeof chunk === "string" ? Buffer.from(chunk) : chunk)); + stream.on("error", reject); + stream.on("end", () => resolve(hash.digest("hex").toLowerCase())); + }); } export async function validateFileAgainstManifest(filePath: string, packageDir: string): Promise<{ ok: boolean; message: string }> { diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index 2462546..008511e 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -155,7 +155,8 @@ export class MegaWebFallback { password, remember: "on" }), - redirect: "manual" + redirect: "manual", + signal: AbortSignal.timeout(30000) }); const cookie = parseSetCookieFromHeaders(response.headers); @@ -169,7 +170,8 @@ export class MegaWebFallback { "User-Agent": "Mozilla/5.0", Cookie: cookie, Referer: DEBRID_REFERER - } + }, + signal: AbortSignal.timeout(30000) }); const verifyHtml = await verify.text(); const hasDebridForm = /id=["']debridForm["']/i.test(verifyHtml) || /name=["']links["']/i.test(verifyHtml); @@ -194,7 +196,8 @@ export class MegaWebFallback { links: link, password: "", showLinks: "1" - }) + }), + signal: AbortSignal.timeout(30000) }); const html = await page.text(); @@ -215,7 +218,8 @@ export class MegaWebFallback { body: new URLSearchParams({ code, autodl: "0" - }) + }), + signal: AbortSignal.timeout(15000) }); const text = (await res.text()).trim(); diff --git a/src/main/realdebrid.ts b/src/main/realdebrid.ts index 7d09941..27ca537 100644 --- a/src/main/realdebrid.ts +++ b/src/main/realdebrid.ts @@ -40,7 +40,8 @@ export class RealDebridClient { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "RD-Node-Downloader/1.1.12" }, - body + body, + signal: AbortSignal.timeout(30000) }); const text = await response.text(); @@ -53,7 +54,12 @@ export class RealDebridClient { throw new Error(parsed); } - const payload = JSON.parse(text) as Record; + let payload: Record; + try { + payload = JSON.parse(text) as Record; + } catch { + throw new Error(`Ungültige JSON-Antwort: ${text.slice(0, 120)}`); + } const directUrl = String(payload.download || payload.link || "").trim(); if (!directUrl) { throw new Error("Unrestrict ohne Download-URL");