Repository: hydralauncher/hydra
File: src/main/services/download/js-http-downloader.ts (TypeScript)

Code before:
      this.status = "paused";
      return;
    }

    const isAbortError = err.name === "AbortError";
    const isRetryable = wasStallRetry || this.isRetryableError(err);
    const canRetry = this.retryCount < MAX_RETRY_ATTEMPTS;

    if (isRetryable && canRetry && !this.isPaused) {
      this.retryCount++;
      const delay = Math.min(
        INITIAL_RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1),
        MAX_RETRY_DELAY_MS
      );

      const reason = wasStallRetry ? "stall detected" : err.message;
      logger.log(
        `[JsHttpDownloader] Retryable error (${reason}). ` +
          `Retry ${this.retryCount}/${MAX_RETRY_ATTEMPTS} in ${delay}ms`
      );

      this.cleanup();
      await this.sleep(delay);

      if (!this.isPaused) {
        await this.startDownloadWithRetry();
      }
    } else if (isAbortError && !wasStallRetry) {
      logger.log("[JsHttpDownloader] Download aborted");
      this.status = "paused";
    } else {
      this.handleDownloadError(err);
    }
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  private prepareDownloadPath(
    savePath: string,
    filename: string | undefined,
    url: string
  ): { filePath: string; startByte: number; usedFallback: boolean } {
    const extractedFilename = filename || this.extractFilename(url);
    const usedFallback = !extractedFilename;
    const resolvedFilename = extractedFilename || "download";
    this.folderName = resolvedFilename;
    const filePath = path.join(savePath, resolvedFilename);

    if (!fs.existsSync(savePath)) {

Review comment (suggestion):
retry logic currently calls `await this.startDownloadWithRetry()` from inside the catch handler, which creates recursion + means the `finally` of the original attempt runs after the retry chain completes. If `cleanup()` mutates global state, the “parent” finally can interfere with the successful retry. Consider a loop-based retry (while/for) so each attempt fully owns its cleanup.

Code after:
      return true;
    }

    return false;
  }

  private async handleDownloadErrorWithRetry(err: Error): Promise<boolean> {
    const wasStallRetry = this.isStallRetry;

    if (this.isPaused && !wasStallRetry) {
      logger.log("[JsHttpDownloader] Download paused by user");
      this.status = "paused";
      return false;
    }

    const isAbortError = err.name === "AbortError";
    const isRetryable = wasStallRetry || this.isRetryableError(err);
    const canRetry = this.retryCount < MAX_RETRY_ATTEMPTS;

    if (isRetryable && canRetry && !this.isPaused) {
      this.retryCount++;
      const delay = Math.min(
        INITIAL_RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1),
        MAX_RETRY_DELAY_MS
      );

      const reason = wasStallRetry ? "stall detected" : err.message;
      logger.log(
        `[JsHttpDownloader] Retryable error (${reason}). ` +
          `Retry ${this.retryCount}/${MAX_RETRY_ATTEMPTS} in ${delay}ms`
      );

      await this.sleep(delay);
      return !this.isPaused;
    }

    if (isAbortError && !wasStallRetry) {
      logger.log("[JsHttpDownloader] Download aborted");
      this.status = "paused";
    } else {
      this.handleDownloadError(err);
    }

    return false;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  private prepareDownloadPath(