DEV Community

Sheng-Lin Yang
Sheng-Lin Yang

Posted on

Combine retry feature and logging in second issue

Repository: CLImanga
Issue: Check chapterImage download response for CLImanga
PR: fix: retry download if failed

During the development of CLImanga, I noticed a small but impactful issue:
When the program tried to download a manga image, if the download failed (for example due to network issues or a temporary server problem), the program would stop immediately and not attempt to retry.

This made the download process unreliable, especially when handling multiple chapters or large manga series.

The goal of this PR was to implement a retry mechanism to improve reliability and provide visibility into download attempts using the custom logging system I implemented in the previous PR.

The retry mechanism needed to satisfy the following:

  1. Attempt downloads multiple times (set a maximum of 5 retries) if an error occurs.
  2. Log each failed attempt with details such as attempt number, URL, and error reason.
  3. Include a small delay between retries to avoid overwhelming the server (time.Sleep).
  4. Fail gracefully if all attempts are exhausted, returning a descriptive error.
  5. Integrate with the existing logging system so all attempts are persisted in logs/latest.log.

I improved the downloadFile function handles the download logic with retry support:

func downloadFile(url, path string) error {
    const maxRetries = 5
    var err error
    for attempt := 0; attempt < maxRetries; attempt++ {
        err = func() error {
            resp, err := http.Get(url)
            if err != nil {
                return err
            }
            defer resp.Body.Close()

            if resp.StatusCode != http.StatusOK {
                return fmt.Errorf("bad status: %s", resp.Status)
            }

            outFile, err := os.Create(path)
            if err != nil {
                return err
            }
            defer outFile.Close()

            _, err = io.Copy(outFile, resp.Body)
            return err
        }()
        if err == nil {
            return nil // success
        }
        logger.Error.Printf("Attempt %d: Failed to download %s: %v", attempt+1, url, err)
        time.Sleep(time.Second * 4) // If failed, wait and retry
    }

    // If all attempts fail, return the last error
    return fmt.Errorf("failed to download file from %s after %d attempts: %v", url, maxRetries, err)
}
Enter fullscreen mode Exit fullscreen mode

Key Design Decisions

  • Anonymous function for each attempt: Ensures that each retry has its own scope for error handling and resource cleanup.
  • Logging with attempt number: Makes it easy to track how many retries occurred and why.
  • Delay using time.Sleep: Reduces risk of overwhelming the server or triggering rate limits.
  • MaxRetries constant: Prevents infinite loops and allows easy adjustment.

After I improved the downloadFile function, I would like to validate the retry mechanism, so I created a unit test:

func TestDownloadFileRetry(t *testing.T) {
  log.Init()
  url := "https://example.com/nonexistentfile.jpg"
  savePath := "test_image.jpg"
  err := downloadFile(url, savePath)
  if err == nil {
    t.Error("It was expected to fail, but it succeeded, which may indicate that the test was invalid.")
  }
}
Enter fullscreen mode Exit fullscreen mode

This point that I did:

  • Uses a URL that is guaranteed to fail (nonexistentfile.jpg)
  • Checks that the function returns an error after retry attempts
  • Confirms that the retry loop is working as expected and logging messages are written

Conclusion

This small PR made a tangible improvement in robustness of CLImanga downloads.
Although the feature is simple, it taught me important lessons about error handling, retries, and structured logging, while allowing me to apply the log system I created in the first PR.

The combination of retry logic and logging ensures users get reliable downloads and developers have clear insight into what went wrong, if anything.

Top comments (0)