diff --git a/context17_test.go b/context17_test.go new file mode 100644 index 00000000..043e1b0a --- /dev/null +++ b/context17_test.go @@ -0,0 +1,9 @@ +// +build !go1.8 + +package resty + +import "strings" + +func errIsContextCanceled(err error) bool { + return strings.Contains(err.Error(), "request canceled") +} diff --git a/context18_test.go b/context18_test.go new file mode 100644 index 00000000..10ec8da6 --- /dev/null +++ b/context18_test.go @@ -0,0 +1,16 @@ +// +build go1.8 + +package resty + +import ( + "context" + "net/url" +) + +func errIsContextCanceled(err error) bool { + ue, ok := err.(*url.Error) + if !ok { + return false + } + return ue.Err == context.Canceled +} diff --git a/context_test.go b/context_test.go new file mode 100644 index 00000000..de84a9fb --- /dev/null +++ b/context_test.go @@ -0,0 +1,191 @@ +// +build go1.7 + +package resty + +import ( + "context" + "net/http" + "testing" + "time" +) + +func TestSetContext(t *testing.T) { + ts := createGetServer(t) + defer ts.Close() + + resp, err := R(). + SetContext(context.Background()). + Get(ts.URL + "/") + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + assertEqual(t, "200 OK", resp.Status()) + assertEqual(t, true, resp.Body() != nil) + assertEqual(t, "TestGet: text response", resp.String()) + + logResponse(t, resp) +} + +func TestSetContextWithError(t *testing.T) { + ts := createGetServer(t) + defer ts.Close() + + resp, err := dcr(). + SetContext(context.Background()). + Get(ts.URL + "/mypage") + + assertError(t, err) + assertEqual(t, http.StatusBadRequest, resp.StatusCode()) + assertEqual(t, "", resp.String()) + + logResponse(t, resp) +} + +func TestSetContextCancel(t *testing.T) { + ch := make(chan struct{}) + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + defer func() { + ch <- struct{}{} // tell test request is finished + }() + t.Logf("Server: %v %v", r.Method, r.URL.Path) + ch <- struct{}{} + <-ch // wait for client to finish request + n, err := w.Write([]byte("TestSetContextCancel: response")) + // FIXME? test server doesn't handle request cancellation + t.Logf("Server: wrote %d bytes", n) + t.Logf("Server: err is %v ", err) + }) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-ch // wait for server to start request handling + cancel() + }() + + _, err := R(). + SetContext(ctx). + Get(ts.URL + "/") + + ch <- struct{}{} // tell server to continue request handling + + <-ch // wait for server to finish request handling + + t.Logf("Error: %v", err) + if !errIsContextCanceled(err) { + t.Errorf("Got unexpected error: %v", err) + } +} + +func TestSetContextCancelRetry(t *testing.T) { + reqCount := 0 + ch := make(chan struct{}) + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + reqCount++ + defer func() { + ch <- struct{}{} // tell test request is finished + }() + t.Logf("Server: %v %v", r.Method, r.URL.Path) + ch <- struct{}{} + <-ch // wait for client to finish request + n, err := w.Write([]byte("TestSetContextCancel: response")) + // FIXME? test server doesn't handle request cancellation + t.Logf("Server: wrote %d bytes", n) + t.Logf("Server: err is %v ", err) + }) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-ch // wait for server to start request handling + cancel() + }() + + c := dc() + c.SetHTTPMode(). + SetTimeout(time.Duration(time.Second * 3)). + SetRetryCount(3) + + _, err := c.R(). + SetContext(ctx). + Get(ts.URL + "/") + + ch <- struct{}{} // tell server to continue request handling + + <-ch // wait for server to finish request handling + + t.Logf("Error: %v", err) + if !errIsContextCanceled(err) { + t.Errorf("Got unexpected error: %v", err) + } + + if reqCount != 1 { + t.Errorf("Request was retried %d times instead of 1", reqCount) + } +} + +func TestSetContextCancelWithError(t *testing.T) { + ch := make(chan struct{}) + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + defer func() { + ch <- struct{}{} // tell test request is finished + }() + t.Logf("Server: %v %v", r.Method, r.URL.Path) + t.Log("Server: sending StatusBadRequest response") + w.WriteHeader(http.StatusBadRequest) + ch <- struct{}{} + <-ch // wait for client to finish request + n, err := w.Write([]byte("TestSetContextCancelWithError: response")) + // FIXME? test server doesn't handle request cancellation + t.Logf("Server: wrote %d bytes", n) + t.Logf("Server: err is %v ", err) + }) + defer ts.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + <-ch // wait for server to start request handling + cancel() + }() + + _, err := R(). + SetContext(ctx). + Get(ts.URL + "/") + + ch <- struct{}{} // tell server to continue request handling + + <-ch // wait for server to finish request handling + + t.Logf("Error: %v", err) + if !errIsContextCanceled(err) { + t.Errorf("Got unexpected error: %v", err) + } +} + +func TestClientRetryWithSetContext(t *testing.T) { + attempt := 0 + ts := createTestServer(func(w http.ResponseWriter, r *http.Request) { + t.Logf("Method: %v", r.Method) + t.Logf("Path: %v", r.URL.Path) + attempt++ + if attempt != 3 { + time.Sleep(time.Second * 2) + } + w.Write([]byte("TestClientRetry page")) + }) + defer ts.Close() + + c := dc() + c.SetHTTPMode(). + SetTimeout(time.Duration(time.Second * 1)). + SetRetryCount(3) + + _, err := c.R(). + SetContext(context.Background()). + Get(ts.URL + "/") + + assertError(t, err) +} diff --git a/middleware.go b/middleware.go index 4ccbffc9..f538863d 100644 --- a/middleware.go +++ b/middleware.go @@ -160,6 +160,9 @@ func createHTTPRequest(c *Client, r *Request) (err error) { r.RawRequest.URL.Host = r.URL } + // Use context if it was specified + r.addContextIfAvailable() + return } diff --git a/request.go b/request.go index 60cbbfb3..6b4b394a 100644 --- a/request.go +++ b/request.go @@ -11,43 +11,11 @@ import ( "encoding/xml" "fmt" "io" - "net/http" "net/url" "reflect" "strings" - "time" ) -// Request type is used to compose and send individual request from client -// go-resty is provide option override client level settings such as -// Auth Token, Basic Auth credentials, Header, Query Param, Form Data, Error object -// and also you can add more options for that particular request -// -type Request struct { - URL string - Method string - QueryParam url.Values - FormData url.Values - Header http.Header - UserInfo *User - Token string - Body interface{} - Result interface{} - Error interface{} - Time time.Time - RawRequest *http.Request - - client *Client - bodyBuf *bytes.Buffer - isMultiPart bool - isFormData bool - setContentLength bool - isSaveResponse bool - outputFile string - proxyURL *url.URL - multipartFiles []*File -} - // SetHeader method is to set a single header field and its value in the current request. // Example: To set `Content-Type` and `Accept` as `application/json`. // resty.R(). @@ -440,6 +408,11 @@ func (r *Request) Execute(method, url string) (*Response, error) { resp, err = r.client.execute(r) if err != nil { r.client.Log.Printf("ERROR [%v] Attempt [%v]", err, attempt) + if r.isContextCancelledIfAvailable() { + // stop Backoff from retrying request if request has been + // canceled by context + return resp, nil + } } return resp, err diff --git a/request16.go b/request16.go new file mode 100644 index 00000000..1292d557 --- /dev/null +++ b/request16.go @@ -0,0 +1,49 @@ +// +build !go1.7 + +package resty + +import ( + "bytes" + "net/http" + "net/url" + "time" +) + +// Request type is used to compose and send individual request from client +// go-resty is provide option override client level settings such as +// Auth Token, Basic Auth credentials, Header, Query Param, Form Data, Error object +// and also you can add more options for that particular request +// +type Request struct { + URL string + Method string + QueryParam url.Values + FormData url.Values + Header http.Header + UserInfo *User + Token string + Body interface{} + Result interface{} + Error interface{} + Time time.Time + RawRequest *http.Request + + client *Client + bodyBuf *bytes.Buffer + isMultiPart bool + isFormData bool + setContentLength bool + isSaveResponse bool + outputFile string + proxyURL *url.URL + multipartFiles []*File +} + +func (r *Request) addContextIfAvailable() { + // nothing to do for golang<1.7 +} + +func (r *Request) isContextCancelledIfAvailable() bool { + // just always return false golang<1.7 + return false +} diff --git a/request17.go b/request17.go new file mode 100644 index 00000000..4303d54c --- /dev/null +++ b/request17.go @@ -0,0 +1,66 @@ +// +build go1.7 + +package resty + +import ( + "bytes" + "context" + "net/http" + "net/url" + "time" +) + +// Request type is used to compose and send individual request from client +// go-resty is provide option override client level settings such as +// Auth Token, Basic Auth credentials, Header, Query Param, Form Data, Error object +// and also you can add more options for that particular request +// +type Request struct { + URL string + Method string + QueryParam url.Values + FormData url.Values + Header http.Header + UserInfo *User + Token string + Body interface{} + Result interface{} + Error interface{} + Time time.Time + RawRequest *http.Request + + client *Client + bodyBuf *bytes.Buffer + isMultiPart bool + isFormData bool + setContentLength bool + isSaveResponse bool + outputFile string + proxyURL *url.URL + multipartFiles []*File + ctx context.Context +} + +// SetContext method sets the context.Context for current Request. It allows +// to interrupt the request execution if ctx.Done() channel is closed. +// See https://blog.golang.org/context article and the "context" package +// documentation. +func (r *Request) SetContext(ctx context.Context) *Request { + r.ctx = ctx + return r +} + +func (r *Request) addContextIfAvailable() { + if r.ctx != nil { + r.RawRequest = r.RawRequest.WithContext(r.ctx) + } +} + +func (r *Request) isContextCancelledIfAvailable() bool { + if r.ctx != nil { + if r.ctx.Err() != nil { + return true + } + } + return false +} diff --git a/resty_test.go b/resty_test.go index 7eb82212..1b2c2bc2 100644 --- a/resty_test.go +++ b/resty_test.go @@ -1280,6 +1280,25 @@ func TestOutputFileAbsPath(t *testing.T) { assertError(t, err) } +func TestContextInternal(t *testing.T) { + ts := createGetServer(t) + defer ts.Close() + + r := R(). + SetQueryParam("request_no", strconv.FormatInt(time.Now().Unix(), 10)) + + if r.isContextCancelledIfAvailable() != false { + t.Error("isContextCancelledIfAvailable != false for vanilla R()") + } + r.addContextIfAvailable() + + resp, err := r.Get(ts.URL + "/") + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + +} + func TestSetTransport(t *testing.T) { ts := createGetServer(t) defer ts.Close()