Repository: hetznercloud/hcloud-go
Pull request #144 (2020-07-28 10:50:59)

Fix: Retry on conflict with request body did not work properly


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index c525a60..4206e43 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@
   service and not providing HTTP-specific options
 * Add `IncludedTraffic`, `OutgoingTraffic` and `IngoingTraffic` fields to `LoadBalancer`
 * Add `ChangeType()` method to the Load Balancer client
+* Fix retrying of requests that contain a body
 
 ## v1.18.2
 
diff --git a/hcloud/client.go b/hcloud/client.go
index cb6d122..fe462aa 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -186,7 +186,21 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re
 // Do performs an HTTP request against the API.
 func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) {
 	var retries int
+	var body []byte
+	var err error
+	if r.ContentLength > 0 {
+		body, err = ioutil.ReadAll(r.Body)
+		if err != nil {
+			r.Body.Close()
+			return nil, err
+		}
+		r.Body.Close()
+	}
 	for {
+		if r.ContentLength > 0 {
+			r.Body = ioutil.NopCloser(bytes.NewReader(body))
+		}
+
 		if c.debugWriter != nil {
 			// To get the response body we need to read it before the request was actually send. https://github.com/golang/go/issues/29792
 			dumpReq, err := httputil.DumpRequestOut(r, true)
@@ -195,6 +209,7 @@ func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) {
 			}
 			fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq)
 		}
+
 		resp, err := c.httpClient.Do(r)
 		if err != nil {
 			return nil, err


Test patch:
diff --git a/hcloud/client_test.go b/hcloud/client_test.go
index 99ddfc4..95cc796 100644
--- a/hcloud/client_test.go
+++ b/hcloud/client_test.go
@@ -1,9 +1,11 @@
 package hcloud
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -261,6 +263,60 @@ func TestClientDo(t *testing.T) {
 	if err != nil {
 		t.Fatalf("unexpected error: %v", err)
 	}
+	if callCount != 2 {
+		t.Fatalf("unexpected callCount: %v", callCount)
+	}
+}
+
+func TestClientDoPost(t *testing.T) {
+	env := newTestEnv()
+	defer env.Teardown()
+
+	env.Client.debugWriter = new(bytes.Buffer)
+	callCount := 0
+	env.Mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
+		callCount++
+		w.Header().Set("Content-Type", "application/json")
+		var dat map[string]interface{}
+		body, err := ioutil.ReadAll(r.Body)
+		r.Body.Close()
+		if err != nil {
+			t.Error(err)
+		}
+		if err := json.Unmarshal(body, &dat); err != nil {
+			t.Error(err)
+		}
+		switch callCount {
+		case 1:
+			if dat["test"] != "abcd" {
+				t.Errorf("unexpected payload: %v", dat)
+			}
+			w.WriteHeader(http.StatusConflict)
+			json.NewEncoder(w).Encode(schema.ErrorResponse{
+				Error: schema.Error{
+					Code:    string(ErrorCodeConflict),
+					Message: "conflict",
+				},
+			})
+		case 2:
+			if dat["test"] != "abcd" {
+				t.Errorf("unexpected payload: %v", dat)
+			}
+			fmt.Fprintln(w, "{}")
+		default:
+			t.Errorf("unexpected number of calls to the test server: %v", callCount)
+		}
+	})
+
+	ctx := context.Background()
+	request, _ := env.Client.NewRequest(ctx, http.MethodPost, "/test", strings.NewReader(`{"test": "abcd"}`))
+	_, err := env.Client.Do(request, nil)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+	if callCount != 2 {
+		t.Fatalf("unexpected callCount: %v", callCount)
+	}
 }
 
 func TestBuildUserAgent(t *testing.T) {