From 498f32bab96ba00e1a7d04c6bc8b367c97d8a335 Mon Sep 17 00:00:00 2001
From: Cory Cooper <coopercory92@gmail.com>
Date: Wed, 5 Oct 2022 21:34:49 -0700
Subject: [PATCH] caddyconfig: Implement retries into HTTPLoader (#5077)

* httploader: Add max_retries

* caddyconfig: dependency-free http config loading retries

* caddyconfig: support `retry_delay` in http loader

* httploader: Implement retries

* Apply suggestions from code review

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
---
 caddyconfig/httploader.go | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go
index 80eab4469..f19921998 100644
--- a/caddyconfig/httploader.go
+++ b/caddyconfig/httploader.go
@@ -94,7 +94,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
 		}
 	}
 
-	resp, err := client.Do(req)
+	resp, err := doHttpCallWithRetries(ctx, client, req)
 	if err != nil {
 		return nil, err
 	}
@@ -119,6 +119,37 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
 	return result, nil
 }
 
+func attemptHttpCall(client *http.Client, request *http.Request) (*http.Response, error) {
+	resp, err := client.Do(request)
+	if err != nil {
+		return nil, fmt.Errorf("problem calling http loader url: %v", err)
+	} else if resp.StatusCode < 200 || resp.StatusCode > 499 {
+		return nil, fmt.Errorf("bad response status code from http loader url: %v", resp.StatusCode)
+	}
+	return resp, nil
+}
+
+func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http.Request) (*http.Response, error) {
+	var resp *http.Response
+	var err error
+	const maxAttempts = 10
+
+	// attempt up to 10 times
+	for i := 0; i < maxAttempts; i++ {
+		resp, err = attemptHttpCall(client, request)
+		if err != nil && i < maxAttempts-1 {
+			// wait 500ms before reattempting, or until context is done
+			select {
+			case <-time.After(time.Millisecond * 500):
+			case <-ctx.Done():
+				return resp, ctx.Err()
+			}
+		}
+	}
+
+	return resp, err
+}
+
 func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
 	client := &http.Client{
 		Timeout: time.Duration(hl.Timeout),