This content originally appeared on DEV Community and was authored by Sheng-Lin Yang
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:
- Attempt downloads multiple times (set a maximum of 5 retries) if an error occurs.
- Log each failed attempt with details such as attempt number, URL, and error reason.
- Include a small delay between retries to avoid overwhelming the server (
time.Sleep). - Fail gracefully if all attempts are exhausted, returning a descriptive error.
- 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)
}
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.")
}
}
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.
This content originally appeared on DEV Community and was authored by Sheng-Lin Yang