DEV Community

Cover image for Downloads Stalling? Not Anymore. Developing Retry Maniac for Chrome
Neeyat Lotlikar
Neeyat Lotlikar

Posted on

Downloads Stalling? Not Anymore. Developing Retry Maniac for Chrome

I'm a Python developer who takes pride in writing code that's more readable and less ambiguous or failure prone. Therefore, it was a never-before seen adventure for me, a journey of treading unknown terrains of JavaScript to get this extension working.

Now, as for the motive that lead to this moment, I was downloading some large files from sites online as a part of my research, and I was disappointed to see that the native download manager wouldn't handle downloads for my use case right. I mean, due to server download speeds, my network's speeds, or whatever else, some of my downloads would stall, and I'd have to manually be there to resume them so they wouldn't give up on me. So, my eyes on the screen most of the time, or they would just fail, and some that wouldn't resume due to the link expiring, maybe. I'm left to now visit the website to get a new download link, some of the files I don't even know from where I started the download for due to ambiguous naming. From here, I knew I needed a manager which would resume my stalled downloads automatically and if they failed, it would retry them for me a couple of times just in case it failed due to a network error. That's how I came to the point of creating an extension of the browsers that would serve this specific use case.

From here on out, I'm going to try my best to describe to you how I made a tool that helped me solve this problem and how you could make a browser extension for yourself, too. I'll also share a link to my code down below so you can refer to the whole thing anytime you like.

To get started, under the root folder, a manifest.json file needs to be defined. Here, we define the permissions we need and tell the browser where all our code files are located. When we load our root folder as an unpackaged extension from the browser's extension page, this file will tell the browser what it needs to know to set things up correctly.

{
  "manifest_version": 3,
  "name": "Retry Maniac: Auto Resume Download",
  "version": "0.1",
  "description": "Automatically resumes interrupted downloads.",
  "permissions": [
    "downloads"
  ],
  "background": {
    "service_worker": "background/sw.js"
  },
  "action": {
    "default_popup": "popup/popup.html"
  },
  "icons": {
    "128": "assets/icon128.png"
  }
}
Enter fullscreen mode Exit fullscreen mode

Under my root folder, I have created directories: popup (containing popup.html file), background (contains sw.js) and assets (carrying the extension's icon's PNG file). As for the permissions, I have added downloads as that's the whole point and notifications which I will generate for really important events.

When you click on the extension's icon, a popup page is displayed, the contents of which are defined in an HTML file. I have issued this page as an informative user description to describe the extension's use cases. The page can be extended further to incorporate dynamic features.

Now for the main event, the star of the show, our code goes in the service worker file, named sw.js, in my case. We define our event listeners here with an intention of overriding the default behavior. In my case, I intend to run my code for resuming an interrupted download.

chrome.downloads.onChanged.addListener(async (delta) => {
    if (!("state" in delta) && !("error" in delta)) return;

    console.log(`Delta: state=${delta.state?.current}, error=${delta.error?.current}`);

    if (delta.state?.current === "interrupted" || delta.error?.current === "NETWORK_FAILED") {
        const [download] = await chrome.downloads.search({ id: delta.id });
        if (!download) return;

        console.log(`Download interrupted: ${download.filename}`);
        await attemptResumeDownload(download);

    } else if (["complete", "cancelled"].includes(delta.state?.current) || delta.exists === false) {
        cleanup(delta.id);
    }
});
Enter fullscreen mode Exit fullscreen mode

In the above snippet, I've added an OnChanged listener to run some code when the state of the download changes. I check here if the download is interrupted or network failed so that if it is, I can attempt to resume this download. I attempt to resume for a set number of tries (five) before finally retrying the download if that fails.

const MAX_RESUME_ATTEMPTS = 5;
const MAX_RETRIES = 3;
const NETWORK_PROBE_INTERVAL = 5000;
Enter fullscreen mode Exit fullscreen mode

The constants initialized at the top of the file are used within the code to assess limits to the number of resumes and retries, and for network probe timeout interval.

async function isNetworkReachable() {
    return navigator.onLine;
}

async function waitForNetwork() {
    while (!(await isNetworkReachable())) {
        console.log(`Network offline, retrying in ${NETWORK_PROBE_INTERVAL}ms`);
        await new Promise(resolve => setTimeout(resolve, NETWORK_PROBE_INTERVAL));
    }
    console.log("Network back online");
}

function chromeDownloadsResumeAsync(downloadId) {
    return new Promise((resolve, reject) => {
        chrome.downloads.resume(downloadId, () => {
            chrome.runtime.lastError ? reject(chrome.runtime.lastError) : resolve();
        });
    });
}

/**
 * Attempt to resume a download if possible, otherwise escalate to retry.
 */
async function attemptResumeDownload(download) {
    if (!(await isNetworkReachable())) {
        console.log(`Network unreachable for ${download.filename}`);
        await waitForNetwork();
    }

    const attempts = (resumeAttempts.get(download.id) || 0) + 1;
    resumeAttempts.set(download.id, attempts);

    if (attempts > MAX_RESUME_ATTEMPTS) {
        console.warn(`Max resume attempts reached for ${download.filename}`);
        await retryDownload(download);
        return;
    }

    try {
        await chromeDownloadsResumeAsync(download.id);
        console.log(`Resumed ${download.filename} (ID: ${download.id}) [Attempt ${attempts}]`);
    } catch (err) {
        console.error(
            `Resume attempt #${attempts} failed for ${download.filename}: ${err.message}`
        );
        // Wait for network and try again unless we've already exhausted
        if (attempts < MAX_RESUME_ATTEMPTS) {
            await waitForNetwork();
            await attemptResumeDownload(download);
        } else {
            console.warn(`Exhausted resume attempts for ${download.filename}, escalating to retry`);
            await retryDownload(download);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I use chromium's navigator.onLine browser-native check to determine if the internet is connected or not. If offline, poll every 5 seconds until the network is online again. Once online, resume the download using Chrome’s native downloads.resume() method.

function cleanup(downloadId) {
    resumeAttempts.delete(downloadId);
    retryCounts.delete(downloadId);
    console.log(`Cleaned up state for download ID: ${downloadId}`);
}
Enter fullscreen mode Exit fullscreen mode

So that no memory is occupied pointlessly, I do clear the counts stored in the global maps by using the cleanup function. It is called after a download is successfully completed or canceled to clear space.

/**
 * Attempt to retry by creating a new download, and reset resume count.
 */
async function retryDownload(download) {
    const retries = retryCounts.get(download.id) || 0;
    if (retries >= MAX_RETRIES || !download.url) {
        console.error(`Max retries reached for ${download.filename}`);
        cleanup(download.id);
        return;
    }

    const newRetryCount = retries + 1;

    chrome.downloads.download({
        url: download.url,
        filename: download.filename,
        conflictAction: "overwrite"
    }, newId => {
        if (newId) {
            // Transfer retry count to new download id
            retryCounts.set(newId, newRetryCount);
            retryCounts.delete(download.id);

            // Reset resume attempt count for the fresh download
            resumeAttempts.set(newId, 0);

            console.log(
                `Retry #${newRetryCount} started for ${download.filename}, new ID: ${newId}`
            );
        } else {
            // Retry creation failed — apply exponential backoff
            console.error(`Failed to create retry for ${download.filename}: ${chrome.runtime.lastError?.message}`);
            const delay = Math.min(1000 * (2 ** retries), 30000);
            setTimeout(() => retryDownload(download), delay);
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Retries attempt a full retry of the download by overwriting or replacing any partial files. By the time I make this blog public, a lot of my code must have dramatically changed. I have documented it well with comments so you can understand if you browse through. Visit my GitHub repo if you want to see the full code.

Retry-Maniac@GitHub

In the future, I intend to store the retry counts persistently across extension reloads through chrome.storage.local usage. Also, implementation of notifications for important events in the download cycle will keep the user informed and make for a better UX.

While I was building this, I learnt that while AI tools make the development process fast, they might introduce unexpected behaviors in the code if you're not careful with the code that it produces. The developer needs to understand the code these assistants produce and make sure it aligns with the requirements of the project. That's just the basic stuff. Some AI generated code may be suboptimal, and it's the developer's responsibility to know when it is as such and make changes to it, accordingly. While it's fast, it may not be correct. AI models may overlook details, it's the developers who need to take care of such things.

I've tested my extension on my downloads and with a few exceptions, the majority of my downloads were completed unlike the last time, without an extension, where the opposite was true. That issue was due to the large number of download tasks running in parallel on the browser and the files also being quite large, but now, my extension saves me from such a mess repeating itself. I must say that now, I can safely leave my PC unattended to download my files knowing well that while download failures may occur due to circumstances that are out of our hands, most of my downloads will be successful because they at least won't stall to death. They will be revived by my extension through automatic resumes. Thank you for reading!

Top comments (0)