From 5b26a3b188dac9110ac458cf106866b22178c289 Mon Sep 17 00:00:00 2001 From: "donggang.crazy" Date: Sun, 24 Mar 2024 15:43:52 +0800 Subject: [PATCH] change retryable type && support retryrev option --- gorequest.go | 314 ++++++++++++++++++++++++---------------------- gorequest_test.go | 67 +++++++++- 2 files changed, 230 insertions(+), 151 deletions(-) diff --git a/gorequest.go b/gorequest.go index a34fc1f..ebb93a1 100644 --- a/gorequest.go +++ b/gorequest.go @@ -55,11 +55,12 @@ const ( ) type superAgentRetryable struct { - RetryableStatus []int - RetryerTime time.Duration - RetryerCount int - Attempt int - Enable bool + RetryableStatus []int + NonRetryableStatus []int + RetryerTime time.Duration + RetryerCount int + Attempt int + Enable bool } // A SuperAgent is a object storing all request data for client. @@ -87,7 +88,7 @@ type SuperAgent struct { Retryable superAgentRetryable DoNotClearSuperAgent bool isClone bool - context context.Context + context context.Context } var DisableTransportSwap = false @@ -195,6 +196,10 @@ func copyRetryable(old superAgentRetryable) superAgentRetryable { for i := range old.RetryableStatus { newRetryable.RetryableStatus[i] = old.RetryableStatus[i] } + newRetryable.NonRetryableStatus = make([]int, len(old.NonRetryableStatus)) + for i := range old.NonRetryableStatus { + newRetryable.NonRetryableStatus[i] = old.NonRetryableStatus[i] + } return newRetryable } @@ -232,7 +237,7 @@ func (s *SuperAgent) Clone() *SuperAgent { Retryable: copyRetryable(s.Retryable), DoNotClearSuperAgent: true, isClone: true, - context: s.context, + context: s.context, } return clone } @@ -368,10 +373,10 @@ func (s *SuperAgent) Options(targetUrl string) *SuperAgent { // this will overwrite the existed values of Header through AppendHeader(). // Example. To set `Accept` as `application/json` // -// gorequest.New(). -// Post("/gamelist"). -// Set("Accept", "application/json"). -// End() +// gorequest.New(). +// Post("/gamelist"). +// Set("Accept", "application/json"). +// End() func (s *SuperAgent) Set(param string, value string) *SuperAgent { s.Header.Set(param, value) return s @@ -380,11 +385,11 @@ func (s *SuperAgent) Set(param string, value string) *SuperAgent { // AppendHeader is used for setting header fileds with multiple values, // Example. To set `Accept` as `application/json, text/plain` // -// gorequest.New(). -// Post("/gamelist"). -// AppendHeader("Accept", "application/json"). -// AppendHeader("Accept", "text/plain"). -// End() +// gorequest.New(). +// Post("/gamelist"). +// AppendHeader("Accept", "application/json"). +// AppendHeader("Accept", "text/plain"). +// End() func (s *SuperAgent) AppendHeader(param string, value string) *SuperAgent { s.Header.Add(param, value) return s @@ -395,10 +400,11 @@ func (s *SuperAgent) AppendHeader(param string, value string) *SuperAgent { // 3 max attempt. // And StatusBadRequest and StatusInternalServerError as RetryableStatus -// gorequest.New(). -// Post("/gamelist"). -// Retry(3, 5 * time.Second, http.StatusBadRequest, http.StatusInternalServerError). -// End() +// gorequest.New(). +// +// Post("/gamelist"). +// Retry(3, 5 * time.Second, http.StatusBadRequest, http.StatusInternalServerError). +// End() func (s *SuperAgent) Retry(retryerCount int, retryerTime time.Duration, statusCode ...int) *SuperAgent { for _, code := range statusCode { statusText := http.StatusText(code) @@ -407,18 +413,30 @@ func (s *SuperAgent) Retry(retryerCount int, retryerTime time.Duration, statusCo } } - s.Retryable = struct { - RetryableStatus []int - RetryerTime time.Duration - RetryerCount int - Attempt int - Enable bool - }{ - statusCode, - retryerTime, - retryerCount, - 0, - true, + s.Retryable = superAgentRetryable{ + RetryableStatus: statusCode, + RetryerTime: retryerTime, + RetryerCount: retryerCount, + Attempt: 0, + Enable: true, + } + return s +} + +func (s *SuperAgent) RetryRev(retryerCount int, retryerTime time.Duration, statusCodeRev ...int) *SuperAgent { + for _, code := range statusCodeRev { + statusText := http.StatusText(code) + if len(statusText) == 0 { + s.Errors = append(s.Errors, errors.New("StatusCode '"+strconv.Itoa(code)+"' doesn't exist in http package")) + } + } + + s.Retryable = superAgentRetryable{ + NonRetryableStatus: statusCodeRev, + RetryerTime: retryerTime, + RetryerCount: retryerCount, + Attempt: 0, + Enable: true, } return s } @@ -426,10 +444,10 @@ func (s *SuperAgent) Retry(retryerCount int, retryerTime time.Duration, statusCo // SetBasicAuth sets the basic authentication header // Example. To set the header for username "myuser" and password "mypass" // -// gorequest.New() -// Post("/gamelist"). -// SetBasicAuth("myuser", "mypass"). -// End() +// gorequest.New() +// Post("/gamelist"). +// SetBasicAuth("myuser", "mypass"). +// End() func (s *SuperAgent) SetBasicAuth(username string, password string) *SuperAgent { s.BasicAuth = struct{ Username, Password string }{username, password} return s @@ -461,22 +479,21 @@ var Types = map[string]string{ // Type is a convenience function to specify the data type to send. // For example, to send data as `application/x-www-form-urlencoded` : // -// gorequest.New(). -// Post("/recipe"). -// Type("form"). -// Send(`{ "name": "egg benedict", "category": "brunch" }`). -// End() +// gorequest.New(). +// Post("/recipe"). +// Type("form"). +// Send(`{ "name": "egg benedict", "category": "brunch" }`). +// End() // // This will POST the body "name=egg benedict&category=brunch" to url /recipe // // GoRequest supports // -// "text/html" uses "html" -// "application/json" uses "json" -// "application/xml" uses "xml" -// "text/plain" uses "text" -// "application/x-www-form-urlencoded" uses "urlencoded", "form" or "form-data" -// +// "text/html" uses "html" +// "application/json" uses "json" +// "application/xml" uses "xml" +// "text/plain" uses "text" +// "application/x-www-form-urlencoded" uses "urlencoded", "form" or "form-data" func (s *SuperAgent) Type(typeStr string) *SuperAgent { if _, ok := Types[typeStr]; ok { s.ForceType = typeStr @@ -489,36 +506,35 @@ func (s *SuperAgent) Type(typeStr string) *SuperAgent { // Query function accepts either json string or strings which will form a query-string in url of GET method or body of POST method. // For example, making "/search?query=bicycle&size=50x50&weight=20kg" using GET method: // -// gorequest.New(). -// Get("/search"). -// Query(`{ query: 'bicycle' }`). -// Query(`{ size: '50x50' }`). -// Query(`{ weight: '20kg' }`). -// End() +// gorequest.New(). +// Get("/search"). +// Query(`{ query: 'bicycle' }`). +// Query(`{ size: '50x50' }`). +// Query(`{ weight: '20kg' }`). +// End() // // Or you can put multiple json values: // -// gorequest.New(). -// Get("/search"). -// Query(`{ query: 'bicycle', size: '50x50', weight: '20kg' }`). -// End() +// gorequest.New(). +// Get("/search"). +// Query(`{ query: 'bicycle', size: '50x50', weight: '20kg' }`). +// End() // // Strings are also acceptable: // -// gorequest.New(). -// Get("/search"). -// Query("query=bicycle&size=50x50"). -// Query("weight=20kg"). -// End() +// gorequest.New(). +// Get("/search"). +// Query("query=bicycle&size=50x50"). +// Query("weight=20kg"). +// End() // // Or even Mixed! :) // -// gorequest.New(). -// Get("/search"). -// Query("query=bicycle"). -// Query(`{ size: '50x50', weight:'20kg' }`). -// End() -// +// gorequest.New(). +// Get("/search"). +// Query("query=bicycle"). +// Query(`{ size: '50x50', weight:'20kg' }`). +// End() func (s *SuperAgent) Query(content interface{}) *SuperAgent { switch v := reflect.ValueOf(content); v.Kind() { case reflect.String: @@ -599,10 +615,9 @@ func (s *SuperAgent) Param(key string, value string) *SuperAgent { // Set TLSClientConfig for underling Transport. // One example is you can use it to disable security check (https): // -// gorequest.New().TLSClientConfig(&tls.Config{ InsecureSkipVerify: true}). -// Get("https://disable-security-check.com"). -// End() -// +// gorequest.New().TLSClientConfig(&tls.Config{ InsecureSkipVerify: true}). +// Get("https://disable-security-check.com"). +// End() func (s *SuperAgent) TLSClientConfig(config *tls.Config) *SuperAgent { s.safeModifyTransport() s.Transport.TLSClientConfig = config @@ -615,16 +630,15 @@ func (s *SuperAgent) TLSClientConfig(config *tls.Config) *SuperAgent { // You will not be able to send different request with different proxy unless you change your `http_proxy` environment again. // Another example is using Golang proxy setting. This is normal prefer way to do but too verbase compared to GoRequest's Proxy: // -// gorequest.New().Proxy("http://myproxy:9999"). -// Post("http://www.google.com"). -// End() +// gorequest.New().Proxy("http://myproxy:9999"). +// Post("http://www.google.com"). +// End() // // To set no_proxy, just put empty string to Proxy func: // -// gorequest.New().Proxy(""). -// Post("http://www.google.com"). -// End() -// +// gorequest.New().Proxy(""). +// Post("http://www.google.com"). +// End() func (s *SuperAgent) Proxy(proxyUrl string) *SuperAgent { parsedProxyUrl, err := url.Parse(proxyUrl) if err != nil { @@ -660,48 +674,47 @@ func (s *SuperAgent) RedirectPolicy(policy func(req Request, via []Request) erro // Send function accepts either json string or query strings which is usually used to assign data to POST or PUT method. // Without specifying any type, if you give Send with json data, you are doing requesting in json format: // -// gorequest.New(). -// Post("/search"). -// Send(`{ query: 'sushi' }`). -// End() +// gorequest.New(). +// Post("/search"). +// Send(`{ query: 'sushi' }`). +// End() // // While if you use at least one of querystring, GoRequest understands and automatically set the Content-Type to `application/x-www-form-urlencoded` // -// gorequest.New(). -// Post("/search"). -// Send("query=tonkatsu"). -// End() +// gorequest.New(). +// Post("/search"). +// Send("query=tonkatsu"). +// End() // // So, if you want to strictly send json format, you need to use Type func to set it as `json` (Please see more details in Type function). // You can also do multiple chain of Send: // -// gorequest.New(). -// Post("/search"). -// Send("query=bicycle&size=50x50"). -// Send(`{ wheel: '4'}`). -// End() +// gorequest.New(). +// Post("/search"). +// Send("query=bicycle&size=50x50"). +// Send(`{ wheel: '4'}`). +// End() // // From v0.2.0, Send function provide another convenience way to work with Struct type. You can mix and match it with json and query string: // -// type BrowserVersionSupport struct { -// Chrome string -// Firefox string -// } -// ver := BrowserVersionSupport{ Chrome: "37.0.2041.6", Firefox: "30.0" } -// gorequest.New(). -// Post("/update_version"). -// Send(ver). -// Send(`{"Safari":"5.1.10"}`). -// End() +// type BrowserVersionSupport struct { +// Chrome string +// Firefox string +// } +// ver := BrowserVersionSupport{ Chrome: "37.0.2041.6", Firefox: "30.0" } +// gorequest.New(). +// Post("/update_version"). +// Send(ver). +// Send(`{"Safari":"5.1.10"}`). +// End() // // If you have set Type to text or Content-Type to text/plain, content will be sent as raw string in body instead of form // -// gorequest.New(). -// Post("/greet"). -// Type("text"). -// Send("hello world"). -// End() -// +// gorequest.New(). +// Post("/greet"). +// Type("text"). +// Send("hello world"). +// End() func (s *SuperAgent) Send(content interface{}) *SuperAgent { // TODO: add normal text mode or other mode to Send func switch v := reflect.ValueOf(content); v.Kind() { @@ -841,51 +854,50 @@ type File struct { // SendFile function works only with type "multipart". The function accepts one mandatory and up to two optional arguments. The mandatory (first) argument is the file. // The function accepts a path to a file as string: // -// gorequest.New(). -// Post("http://example.com"). -// Type("multipart"). -// SendFile("./example_file.ext"). -// End() +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile("./example_file.ext"). +// End() // // File can also be a []byte slice of a already file read by eg. ioutil.ReadFile: // -// b, _ := ioutil.ReadFile("./example_file.ext") -// gorequest.New(). -// Post("http://example.com"). -// Type("multipart"). -// SendFile(b). -// End() +// b, _ := ioutil.ReadFile("./example_file.ext") +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile(b). +// End() // // Furthermore file can also be a os.File: // -// f, _ := os.Open("./example_file.ext") -// gorequest.New(). -// Post("http://example.com"). -// Type("multipart"). -// SendFile(f). -// End() +// f, _ := os.Open("./example_file.ext") +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile(f). +// End() // // The first optional argument (second argument overall) is the filename, which will be automatically determined when file is a string (path) or a os.File. // When file is a []byte slice, filename defaults to "filename". In all cases the automatically determined filename can be overwritten: // -// b, _ := ioutil.ReadFile("./example_file.ext") -// gorequest.New(). -// Post("http://example.com"). -// Type("multipart"). -// SendFile(b, "my_custom_filename"). -// End() +// b, _ := ioutil.ReadFile("./example_file.ext") +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile(b, "my_custom_filename"). +// End() // // The second optional argument (third argument overall) is the fieldname in the multipart/form-data request. It defaults to fileNUMBER (eg. file1), where number is ascending and starts counting at 1. // So if you send multiple files, the fieldnames will be file1, file2, ... unless it is overwritten. If fieldname is set to "file" it will be automatically set to fileNUMBER, where number is the greatest exsiting number+1 unless // a third argument skipFileNumbering is provided and true. // -// b, _ := ioutil.ReadFile("./example_file.ext") -// gorequest.New(). -// Post("http://example.com"). -// Type("multipart"). -// SendFile(b, "", "my_custom_fieldname"). // filename left blank, will become "example_file.ext" -// End() -// +// b, _ := ioutil.ReadFile("./example_file.ext") +// gorequest.New(). +// Post("http://example.com"). +// Type("multipart"). +// SendFile(b, "", "my_custom_fieldname"). // filename left blank, will become "example_file.ext" +// End() func (s *SuperAgent) SendFile(file interface{}, args ...interface{}) *SuperAgent { filename := "" @@ -1063,22 +1075,21 @@ func changeMapToURLValues(data map[string]interface{}) url.Values { // // For example: // -// resp, body, errs := gorequest.New().Get("http://www.google.com").End() -// if errs != nil { -// fmt.Println(errs) -// } -// fmt.Println(resp, body) +// resp, body, errs := gorequest.New().Get("http://www.google.com").End() +// if errs != nil { +// fmt.Println(errs) +// } +// fmt.Println(resp, body) // // Moreover, End function also supports callback which you can put as a parameter. // This extends the flexibility and makes GoRequest fun and clean! You can use GoRequest in whatever style you love! // // For example: // -// func printBody(resp gorequest.Response, body string, errs []error){ -// fmt.Println(resp.Status) -// } -// gorequest.New().Get("http://www..google.com").End(printBody) -// +// func printBody(resp gorequest.Response, body string, errs []error){ +// fmt.Println(resp.Status) +// } +// gorequest.New().Get("http://www..google.com").End(printBody) func (s *SuperAgent) End(callback ...func(response Response, body string, errs []error)) (Response, string, []error) { var bytesCallback []func(response Response, body []byte, errs []error) if len(callback) > 0 { @@ -1122,10 +1133,13 @@ func (s *SuperAgent) EndBytes(callback ...func(response Response, body []byte, e } func (s *SuperAgent) isRetryableRequest(resp Response) bool { - if s.Retryable.Enable && s.Retryable.Attempt < s.Retryable.RetryerCount && contains(resp.StatusCode, s.Retryable.RetryableStatus) { + if s.Retryable.Enable && s.Retryable.Attempt < s.Retryable.RetryerCount && + (contains(resp.StatusCode, s.Retryable.RetryableStatus) || + !contains(resp.StatusCode, s.Retryable.NonRetryableStatus)) { time.Sleep(s.Retryable.RetryerTime) s.Retryable.Attempt++ return false + } return true } @@ -1139,7 +1153,7 @@ func contains(respStatus int, statuses []int) bool { return false } -func (s *SuperAgent) Context(ctx context.Context) *SuperAgent{ +func (s *SuperAgent) Context(ctx context.Context) *SuperAgent { if ctx == nil { panic("context can't be nil") } diff --git a/gorequest_test.go b/gorequest_test.go index bd0056b..6fc74cb 100644 --- a/gorequest_test.go +++ b/gorequest_test.go @@ -608,6 +608,72 @@ func TestRetryGet(t *testing.T) { } } +// testing for Get method with retryRev option +func TestRetryRevGet(t *testing.T) { + const ( + case1_empty = "/" + case24_after_3_attempt_return_valid = "/retry_3_attempt_then_valid" + retry_count_expected = "3" + ) + + var attempt int + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // check method is GET before going to check other features + if r.Method != GET { + t.Errorf("Expected method %q; got %q", GET, r.Method) + } + + //set return status + + if r.Header == nil { + t.Error("Expected non-nil request Header") + } + switch r.URL.Path { + default: + t.Errorf("No testing for this case yet : %q", r.URL.Path) + case case1_empty: + w.WriteHeader(http.StatusForbidden) + t.Logf("case %v ", case1_empty) + case case24_after_3_attempt_return_valid: + if attempt == 3 { + w.WriteHeader(200) + } else { + w.WriteHeader(http.StatusBadRequest) + t.Logf("case %v ", case24_after_3_attempt_return_valid) + } + attempt++ + } + + })) + + defer ts.Close() + + resp, _, errs := New().Get(ts.URL+case1_empty). + RetryRev(3, 1*time.Nanosecond, http.StatusOK). + End() + if errs != nil { + t.Errorf("No testing for this case yet : %q", errs) + } + + retryCountReturn := resp.Header.Get("Retry-Count") + if retryCountReturn != retry_count_expected { + t.Errorf("Expected [%s] retry but was [%s]", retry_count_expected, retryCountReturn) + } + + resp, _, errs = New().Get(ts.URL+case24_after_3_attempt_return_valid). + RetryRev(4, 1*time.Nanosecond, http.StatusOK). + End() + if errs != nil { + t.Errorf("No testing for this case yet : %q", errs) + } + + retryCountReturn = resp.Header.Get("Retry-Count") + if retryCountReturn != retry_count_expected { + t.Errorf("Expected [%s] retry but was [%s]", retry_count_expected, retryCountReturn) + } +} + // testing for Options method func TestOptions(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -2593,7 +2659,6 @@ func TestContenxt(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) go New().Context(ctx).Get("http://127.0.0.1:8080/foo").EndBytes(func(response Response, body []byte, errs []error) { - if len(errs) > 0 { fmt.Printf("%+v\n", errs[0]) }