Repository: go-resty/resty (go)

Issue:
Upload empty file get "EOF" error
```golang
	result := &app.AFSRetInfo{}
	url := fmt.Sprintf("http://%s/v1/game_volume/upload_file", u.Host)

	srcBytes, err := ioutil.ReadFile(u.SrcPath)
	if err != nil {
		s := fmt.Sprintf("UploadFileInfo.Do: ReadFile faild, url[%s] u[%+v] err[%v]", url, u, err)
		vlog.Error(s)
		return errors.New(s)
	}

	req := client.R().SetFormData(map[string]string{"file_path": u.DestPath}).
		SetFileReader("file", filepath.Base(u.SrcPath), bytes.NewReader(srcBytes)).
		SetContentLength(true).
		SetResult(result)
```

upload empty file error

```shell
ERROR	AStorage.UploadHandle: UpLoadFileList faild, err[UploadFileInfo.Do: faild, url[http://10.86.22.118:48885/v1/game_volume/upload_file] u[&{Host:10.86.22.118:48885 SrcPath:/Users/haibei/vrviuworks/code/be_gsm_tester/data/snapshot/area_user_game_data/2_642_1/data/Shooty Skies/moregames/kong DestPath:/data/data/test/kongfile/kong}] err[EOF]]	{"LAPP": "vm_game_planner", "LPID": 21068, "LFILE": "v1/android.go", "LLINE": 44, "LIP": ""}
```


Pull request:
Handle EOF from multipart form file readers
I stumbled on a bug when trying to send a multipart file that happened to be a gzip reader downloaded from elsewhere. Looks like the multipart content type detection logic doesn't handle `EOF` correctly. This can also be easily reproduced by sending an empty file (`bytes.NewReader(nil)`).

Here's a self contained example that reproduces the issue with a gzipped string:
```go
b := bytes.NewBuffer(nil)
w := gzip.NewWriter(b)
if _, err := w.Write([]byte("foo")); err != nil {
	panic(err)
}
if err := w.Close(); err != nil {
	panic(err)
}
r, err := gzip.NewReader(b)
if err != nil {
	panic(err)
}
_, err = resty.New().NewRequest().SetFileReader("file", "foo.bar", r).Post("http://foo.bar")
if err != nil {
	panic(err) // EOF!
}
```

----

Closes #431 

Patch:
diff --git a/retry.go b/retry.go
index a841c46..00b8514 100644
--- a/retry.go
+++ b/retry.go
@@ -129,6 +129,12 @@ func Backoff(operation func() (*Response, error), options ...Option) error {
 			hook(resp, err)
 		}
 
+		// Don't need to wait when no retries left.
+		// Still run retry hooks even on last retry to keep compatibility.
+		if attempt == opts.maxRetries {
+			return err
+		}
+
 		waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
 		if err2 != nil {
 			if err == nil {
diff --git a/util.go b/util.go
index d0abb4e..a1010fb 100644
--- a/util.go
+++ b/util.go
@@ -202,7 +202,7 @@ func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r i
 	// Auto detect actual multipart content type
 	cbuf := make([]byte, 512)
 	size, err := r.Read(cbuf)
-	if err != nil {
+	if err != nil && err != io.EOF {
 		return err
 	}
 


Test patch:
diff --git a/retry_test.go b/retry_test.go
index e302216..9f8fb38 100644
--- a/retry_test.go
+++ b/retry_test.go
@@ -32,6 +32,39 @@ func TestBackoffSuccess(t *testing.T) {
 	assertEqual(t, externalCounter, attempts)
 }
 
+func TestBackoffNoWaitForLastRetry(t *testing.T) {
+	attempts := 1
+	externalCounter := 0
+	numRetries := 1
+
+	canceledCtx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	resp := &Response{
+		Request: &Request{
+			ctx: canceledCtx,
+			client: &Client{
+				RetryAfter: func(*Client, *Response) (time.Duration, error) {
+					return 6, nil
+				},
+			},
+		},
+	}
+
+	retryErr := Backoff(func() (*Response, error) {
+		externalCounter++
+		return resp, nil
+	}, RetryConditions([]RetryConditionFunc{func(response *Response, err error) bool {
+		if externalCounter == attempts + numRetries {
+			// Backoff returns context canceled if goes to sleep after last retry.
+			cancel()
+		}
+		return true
+	}}), Retries(numRetries))
+
+	assertNil(t, retryErr)
+}
+
 func TestBackoffTenAttemptsSuccess(t *testing.T) {
 	attempts := 10
 	externalCounter := 0
@@ -169,8 +202,8 @@ func TestClientRetryGet(t *testing.T) {
 	assertNotNil(t, resp.Body())
 	assertEqual(t, 0, len(resp.Header()))
 
-	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
-		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+	assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
 }
 
 func TestClientRetryWait(t *testing.T) {
@@ -639,8 +672,8 @@ func TestClientRetryCount(t *testing.T) {
 	// 2 attempts were made
 	assertEqual(t, attempt, 2)
 
-	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
-		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+	assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
 }
 
 func TestClientErrorRetry(t *testing.T) {
@@ -693,8 +726,8 @@ func TestClientRetryHook(t *testing.T) {
 
 	assertEqual(t, 3, attempt)
 
-	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
-		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+	assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
 }
 
 func filler(*Response, error) bool {
diff --git a/util_test.go b/util_test.go
index e5690d4..ef2bb91 100644
--- a/util_test.go
+++ b/util_test.go
@@ -5,6 +5,8 @@
 package resty
 
 import (
+	"bytes"
+	"mime/multipart"
 	"testing"
 )
 
@@ -79,3 +81,17 @@ func TestIsXMLType(t *testing.T) {
 		}
 	}
 }
+
+func TestWriteMultipartFormFileReaderEmpty(t *testing.T) {
+	w := multipart.NewWriter(bytes.NewBuffer(nil))
+	defer func() { _ = w.Close() }()
+	if err := writeMultipartFormFile(w, "foo", "bar", bytes.NewReader(nil)); err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+}
+
+func TestWriteMultipartFormFileReaderError(t *testing.T) {
+	err := writeMultipartFormFile(nil, "", "", &brokenReadCloser{})
+	assertNotNil(t, err)
+	assertEqual(t, "read error", err.Error())
+}