diff --git a/pkg/modules/migration/helpers.go b/pkg/modules/migration/helpers.go index fb6d400aa..835d14b60 100644 --- a/pkg/modules/migration/helpers.go +++ b/pkg/modules/migration/helpers.go @@ -20,6 +20,8 @@ import ( "bytes" "context" "crypto/rand" + "fmt" + "io" "math" "math/big" "net/http" @@ -92,9 +94,19 @@ func DoPostWithHeaders(url string, form url.Values, headers map[string]string) ( return resp, nil } - // Don't retry on last attempt + // Return error on last attempt if still getting 5xx if attempt == maxRetries-1 { - return resp, nil + bodyBytes, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + + // Re-create the body so the caller can still read it if needed + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + if readErr != nil { + return resp, fmt.Errorf("request failed after %d attempts with status code %d (could not read response body: %w)", maxRetries, resp.StatusCode, readErr) + } + + return resp, fmt.Errorf("request failed after %d attempts with status code %d: %s", maxRetries, resp.StatusCode, string(bodyBytes)) } // Close the body before retrying @@ -108,5 +120,5 @@ func DoPostWithHeaders(url string, form url.Values, headers map[string]string) ( time.Sleep(delay + jitter) } - return resp, nil + return nil, fmt.Errorf("request failed after %d attempts", maxRetries) } diff --git a/pkg/modules/migration/helpers_test.go b/pkg/modules/migration/helpers_test.go index 9f4cf9e19..867168b71 100644 --- a/pkg/modules/migration/helpers_test.go +++ b/pkg/modules/migration/helpers_test.go @@ -20,6 +20,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strings" "sync/atomic" "testing" ) @@ -53,17 +54,25 @@ func TestDoPostWithHeaders_RetriesOn500(t *testing.T) { func TestDoPostWithHeaders_GivesUpAfter3Retries(t *testing.T) { var attempts atomic.Int32 + expectedBody := "Internal Server Error: database connection failed" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { attempts.Add(1) w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(expectedBody)) })) defer server.Close() form := url.Values{"key": {"value"}} resp, err := DoPostWithHeaders(server.URL, form, map[string]string{}) - if err != nil { - t.Fatalf("expected no error, got %v", err) + if err == nil { + t.Fatal("expected error after exhausted retries, got nil") + } + if !strings.Contains(err.Error(), expectedBody) { + t.Errorf("expected error message to contain response body %q, got: %s", expectedBody, err.Error()) + } + if resp == nil { + t.Fatal("expected response to be returned with error, got nil") } defer resp.Body.Close() if resp.StatusCode != http.StatusInternalServerError {