diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 224dc3d3..3ea8441d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: diff -u <(echo -n) <(go fmt $(go list ./...)) - name: Test - run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic + run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml index 5c4c8734..5747117a 100644 --- a/.github/workflows/label-actions.yml +++ b/.github/workflows/label-actions.yml @@ -29,7 +29,7 @@ jobs: cache-dependency-path: go.sum - name: Test - run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic + run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... - name: Coverage run: bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore index 9e856bd4..7542ac89 100644 --- a/.gitignore +++ b/.gitignore @@ -26,5 +26,6 @@ _testmain.go coverage.out coverage.txt -# Exclude intellij IDE folders +# Exclude IDE folders .idea/* +.vscode/* diff --git a/BUILD.bazel b/BUILD.bazel index f461c29d..e41515b7 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -22,6 +22,8 @@ go_library( "transport.go", "transport112.go", "util.go", + "util_curl.go", + "shellescape/shellescape.go" ], importpath = "github.com/go-resty/resty/v2", visibility = ["//visibility:public"], diff --git a/LICENSE b/LICENSE index 0c2d38a3..de30fea8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2023 Jeevanandam M., https://myjeeva.com +Copyright (c) 2015-2024 Jeevanandam M., https://myjeeva.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index eccca8bc..2d3542b5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Features section describes in detail about Resty capabilities

-

Build Status Code Coverage Go Report Card Release Version GoDoc License Mentioned in Awesome Go

+

Build Status Code Coverage Go Report Card Release Version GoDoc License Mentioned in Awesome Go

Resty Communication Channels

@@ -13,7 +13,7 @@ ## News - * v2.13.1 [released](https://github.com/go-resty/resty/releases/tag/v2.13.1) and tagged on May 10, 2024. + * v2.14.0 [released](https://github.com/go-resty/resty/releases/tag/v2.14.0) and tagged on Aug 04, 2024. * v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019. * v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019. * v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors). @@ -131,6 +131,11 @@ client := resty.New() resp, err := client.R(). EnableTrace(). Get("https://httpbin.org/get") +curlCmdExecuted := resp.Request.GenerateCurlCommand() + + +// Explore curl command +fmt.Println("Curl Command:\n ", curlCmdExecuted+"\n") // Explore response object fmt.Println("Response Info:") @@ -160,6 +165,9 @@ fmt.Println(" RequestAttempt:", ti.RequestAttempt) fmt.Println(" RemoteAddr :", ti.RemoteAddr.String()) /* Output +Curl Command: + curl -X GET -H 'User-Agent: go-resty/2.12.0 (https://github.com/go-resty/resty)' https://httpbin.org/get + Response Info: Error : Status Code: 200 diff --git a/client.go b/client.go index 1bcafba8..aeb55077 100644 --- a/client.go +++ b/client.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. @@ -1148,9 +1148,7 @@ func (c *Client) Clone() *Client { // Client Unexported methods //_______________________________________________________________________ -// Executes method executes the given `Request` object and returns response -// error. -func (c *Client) execute(req *Request) (*Response, error) { +func (c *Client) executeBefore(req *Request) error { // Lock the user-defined pre-request hooks. c.udBeforeRequestLock.RLock() defer c.udBeforeRequestLock.RUnlock() @@ -1166,7 +1164,7 @@ func (c *Client) execute(req *Request) (*Response, error) { // to modify the *resty.Request object for _, f := range c.udBeforeRequest { if err = f(c, req); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } } @@ -1174,14 +1172,14 @@ func (c *Client) execute(req *Request) (*Response, error) { // will return an error if the rate limit is exceeded. if req.client.rateLimiter != nil { if !req.client.rateLimiter.Allow() { - return nil, wrapNoRetryErr(ErrRateLimitExceeded) + return wrapNoRetryErr(ErrRateLimitExceeded) } } // resty middlewares for _, f := range c.beforeRequest { if err = f(c, req); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } } @@ -1192,15 +1190,24 @@ func (c *Client) execute(req *Request) (*Response, error) { // call pre-request if defined if c.preReqHook != nil { if err = c.preReqHook(c, req.RawRequest); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } } if err = requestLogger(c, req); err != nil { - return nil, wrapNoRetryErr(err) + return wrapNoRetryErr(err) } req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf) + return nil +} + +// Executes method executes the given `Request` object and returns response +// error. +func (c *Client) execute(req *Request) (*Response, error) { + if err := c.executeBefore(req); err != nil { + return nil, err + } req.Time = time.Now() resp, err := c.httpClient.Do(req.RawRequest) @@ -1396,6 +1403,7 @@ func createClient(hc *http.Client) *Client { parseRequestBody, createHTTPRequest, addCredentials, + createCurlCmd, } // user defined request middlewares diff --git a/client_test.go b/client_test.go index 6ea3dbd5..c89d932d 100644 --- a/client_test.go +++ b/client_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/context_test.go b/context_test.go index f3c53450..5d60cfff 100644 --- a/context_test.go +++ b/context_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com) +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com) // 2016 Andrew Grigorev (https://github.com/ei-grad) // All rights reserved. // resty source code and usage is governed by a MIT style diff --git a/digest.go b/digest.go index 3cd19637..3a08477d 100644 --- a/digest.go +++ b/digest.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com) +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com) // 2023 Segev Dagan (https://github.com/segevda) // 2024 Philipp Wolfer (https://github.com/phw) // All rights reserved. diff --git a/example_test.go b/example_test.go index 7608f1df..7b7514de 100644 --- a/example_test.go +++ b/example_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M. (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M. (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/examples/debug_curl_test.go b/examples/debug_curl_test.go new file mode 100644 index 00000000..f506ae17 --- /dev/null +++ b/examples/debug_curl_test.go @@ -0,0 +1,126 @@ +package examples + +import ( + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/go-resty/resty/v2" +) + +// 1. Generate curl for unexecuted request(dry-run) +func TestGenerateUnexcutedCurl(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + req := resty.New().R().SetBody(map[string]string{ + "name": "Alex", + }).SetCookies( + []*http.Cookie{ + {Name: "count", Value: "1"}, + }, + ) + + curlCmdUnexecuted := req.GenerateCurlCommand() + + if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") || + !strings.Contains(curlCmdUnexecuted, "curl -X GET") || + !strings.Contains(curlCmdUnexecuted, `-d '{"name":"Alex"}'`) { + t.Fatal("Incomplete curl:", curlCmdUnexecuted) + } else { + t.Log("curlCmdUnexecuted: \n", curlCmdUnexecuted) + } + +} + +// 2. Generate curl for executed request +func TestGenerateExecutedCurl(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + data := map[string]string{ + "name": "Alex", + } + req := resty.New().R().SetBody(data).SetCookies( + []*http.Cookie{ + {Name: "count", Value: "1"}, + }, + ) + + url := ts.URL + "/post" + resp, err := req. + EnableTrace(). + Post(url) + if err != nil { + t.Fatal(err) + } + curlCmdExecuted := resp.Request.GenerateCurlCommand() + if !strings.Contains(curlCmdExecuted, "Cookie: count=1") || + !strings.Contains(curlCmdExecuted, "curl -X POST") || + !strings.Contains(curlCmdExecuted, `-d '{"name":"Alex"}'`) || + !strings.Contains(curlCmdExecuted, url) { + t.Fatal("Incomplete curl:", curlCmdExecuted) + } else { + t.Log("curlCmdExecuted: \n", curlCmdExecuted) + } +} + +// 3. Generate curl in debug mode +func TestDebugModeCurl(t *testing.T) { + ts := createHttpbinServer(0) + defer ts.Close() + + // 1. Capture stderr + getOutput, restore := captureStderr() + defer restore() + + // 2. Build request + req := resty.New().R().SetBody(map[string]string{ + "name": "Alex", + }).SetCookies( + []*http.Cookie{ + {Name: "count", Value: "1"}, + }, + ) + + // 3. Execute request: set debug mode + url := ts.URL + "/post" + _, err := req.SetDebug(true).Post(url) + if err != nil { + t.Fatal(err) + } + + // 4. test output curl + output := getOutput() + if !strings.Contains(output, "Cookie: count=1") || + !strings.Contains(output, `-d '{"name":"Alex"}'`) { + t.Fatal("Incomplete debug curl info:", output) + } else { + t.Log("Normal debug curl info: \n", output) + } +} + +func captureStderr() (getOutput func() string, restore func()) { + old := os.Stderr + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + os.Stderr = w + getOutput = func() string { + w.Close() + buf := make([]byte, 2048) + n, err := r.Read(buf) + if err != nil && err != io.EOF { + panic(err) + } + return string(buf[:n]) + } + restore = func() { + os.Stderr = old + w.Close() + } + return getOutput, restore +} diff --git a/examples/server_test.go b/examples/server_test.go new file mode 100644 index 00000000..285f8b64 --- /dev/null +++ b/examples/server_test.go @@ -0,0 +1,162 @@ +package examples + +import ( + "bytes" + "encoding/json" + "fmt" + ioutil "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" +) + +const maxMultipartMemory = 4 << 30 // 4MB + +// tlsCert: +// +// 0 No certificate +// 1 With self-signed certificate +// 2 With custom certificate from CA(todo) +func createHttpbinServer(tlsCert int) (ts *httptest.Server) { + ts = createTestServer(func(w http.ResponseWriter, r *http.Request) { + httpbinHandler(w, r) + }, tlsCert) + + return ts +} + +func httpbinHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := ioutil.ReadAll(r.Body) + r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // important!! + m := map[string]interface{}{ + "args": parseRequestArgs(r), + "headers": dumpRequestHeader(r), + "data": string(body), + "json": nil, + "form": map[string]string{}, + "files": map[string]string{}, + "method": r.Method, + "origin": r.RemoteAddr, + "url": r.URL.String(), + } + + // 1. parse text/plain + if strings.HasPrefix(r.Header.Get("Content-Type"), "text/plain") { + m["data"] = string(body) + } + + // 2. parse application/json + if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") { + var data interface{} + if err := json.Unmarshal(body, &data); err != nil { + m["err"] = err.Error() + } else { + m["json"] = data + } + } + + // 3. parse application/x-www-form-urlencoded + if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") { + m["form"] = parseQueryString(string(body)) + } + + // 4. parse multipart/form-data + if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") { + form, files := readMultipartForm(r) + m["form"] = form + m["files"] = files + } + buf, _ := json.Marshal(m) + _, _ = w.Write(buf) +} + +func readMultipartForm(r *http.Request) (map[string]string, map[string]string) { + if err := r.ParseMultipartForm(maxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + panic(fmt.Sprintf("error on parse multipart form array: %v", err)) + } + } + // parse form data + formData := make(map[string]string) + for k, vs := range r.PostForm { + for _, v := range vs { + formData[k] = v + } + } + // parse files + files := make(map[string]string) + if r.MultipartForm != nil && r.MultipartForm.File != nil { + for key, fhs := range r.MultipartForm.File { + // if len(fhs)>0 + // f, err := fhs[0].Open() + files[key] = fhs[0].Filename + } + } + return formData, files +} + +func dumpRequestHeader(req *http.Request) string { + var res strings.Builder + headers := sortHeaders(req) + for _, kv := range headers { + res.WriteString(kv[0] + ": " + kv[1] + "\n") + } + return res.String() +} + +// sortHeaders +func sortHeaders(request *http.Request) [][2]string { + headers := [][2]string{} + for k, vs := range request.Header { + for _, v := range vs { + headers = append(headers, [2]string{k, v}) + } + } + n := len(headers) + for i := 0; i < n; i++ { + for j := n - 1; j > i; j-- { + jj := j - 1 + h1, h2 := headers[j], headers[jj] + if h1[0] < h2[0] { + headers[jj], headers[j] = headers[j], headers[jj] + } + } + } + return headers +} + +func parseRequestArgs(request *http.Request) map[string]string { + query := request.URL.RawQuery + return parseQueryString(query) +} + +func parseQueryString(query string) map[string]string { + params := map[string]string{} + paramsList, _ := url.ParseQuery(query) + for key, vals := range paramsList { + // params[key] = vals[len(vals)-1] + params[key] = strings.Join(vals, ",") + } + return params +} + +/* +* + - tlsCert: + 0 no certificate + 1 with self-signed certificate + 2 with custom certificate from CA(todo) +*/ +func createTestServer(fn func(w http.ResponseWriter, r *http.Request), tlsCert int) (ts *httptest.Server) { + if tlsCert == 0 { + // 1. http test server + ts = httptest.NewServer(http.HandlerFunc(fn)) + } else if tlsCert == 1 { + // 2. https test server: https://stackoverflow.com/questions/54899550/create-https-test-server-for-any-client + ts = httptest.NewUnstartedServer(http.HandlerFunc(fn)) + ts.StartTLS() + } + return ts +} diff --git a/go.mod b/go.mod index 1dca97c8..442c4ee6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/go-resty/resty/v2 go 1.16 require ( - golang.org/x/net v0.25.0 - golang.org/x/time v0.5.0 + golang.org/x/net v0.27.0 + golang.org/x/time v0.6.0 ) diff --git a/go.sum b/go.sum index e21530d7..2d51ea97 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,32 @@ +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -23,25 +34,34 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/middleware.go b/middleware.go index 603448df..41e6561f 100644 --- a/middleware.go +++ b/middleware.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. @@ -307,6 +307,16 @@ func addCredentials(c *Client, r *Request) error { return nil } +func createCurlCmd(c *Client, r *Request) (err error) { + if r.trace { + if r.resultCurlCmd == nil { + r.resultCurlCmd = new(string) + } + *r.resultCurlCmd = buildCurlRequest(r.RawRequest, c.httpClient.Jar) + } + return nil +} + func requestLogger(c *Client, r *Request) error { if r.Debug { rr := r.RawRequest @@ -329,6 +339,8 @@ func requestLogger(c *Client, r *Request) error { } reqLog := "\n==============================================================================\n" + + "~~~ REQUEST(curl) ~~~\n" + + fmt.Sprintf("CURL:\n %v\n", buildCurlRequest(r.RawRequest, r.client.httpClient.Jar)) + "~~~ REQUEST ~~~\n" + fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) + fmt.Sprintf("HOST : %s\n", rr.URL.Host) + diff --git a/redirect.go b/redirect.go index ed58d735..19bd587d 100644 --- a/redirect.go +++ b/redirect.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/request.go b/request.go index 4e13ff09..336925d5 100644 --- a/request.go +++ b/request.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. @@ -39,6 +39,7 @@ type Request struct { Time time.Time Body interface{} Result interface{} + resultCurlCmd *string Error interface{} RawRequest *http.Request SRV *SRVRecord @@ -73,6 +74,24 @@ type Request struct { retryConditions []RetryConditionFunc } +// Generate curl command for the request. +func (r *Request) GenerateCurlCommand() string { + if r.resultCurlCmd != nil { + return *r.resultCurlCmd + } else { + if r.RawRequest == nil { + r.client.executeBefore(r) // mock with r.Get("/") + } + if r.resultCurlCmd == nil { + r.resultCurlCmd = new(string) + } + if *r.resultCurlCmd == "" { + *r.resultCurlCmd = buildCurlRequest(r.RawRequest, r.client.httpClient.Jar) + } + return *r.resultCurlCmd + } +} + // Context method returns the Context if its already set in request // otherwise it creates new one using `context.Background()`. func (r *Request) Context() context.Context { @@ -886,7 +905,7 @@ func (r *Request) Patch(url string) (*Response, error) { // for current `Request`. // // req := client.R() -// req.Method = resty.GET +// req.Method = resty.MethodGet // req.URL = "http://httpbin.org/get" // resp, err := req.Send() func (r *Request) Send() (*Response, error) { @@ -896,7 +915,7 @@ func (r *Request) Send() (*Response, error) { // Execute method performs the HTTP request with given HTTP method and URL // for current `Request`. // -// resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get") +// resp, err := client.R().Execute(resty.MethodGet, "http://httpbin.org/get") func (r *Request) Execute(method, url string) (*Response, error) { var addrs []*net.SRV var resp *Response diff --git a/request_test.go b/request_test.go index 7312128f..518839b7 100644 --- a/request_test.go +++ b/request_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/response.go b/response.go index 63c95c41..58a8e816 100644 --- a/response.go +++ b/response.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/resty.go b/resty.go index f8becec6..7458af76 100644 --- a/resty.go +++ b/resty.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. @@ -14,7 +14,7 @@ import ( ) // Version # of resty -const Version = "2.13.1" +const Version = "2.14.0" // New method creates a new Resty client. func New() *Client { diff --git a/resty_test.go b/resty_test.go index c00ad1fb..22b483c1 100644 --- a/resty_test.go +++ b/resty_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/retry.go b/retry.go index c5eda26b..932a266d 100644 --- a/retry.go +++ b/retry.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/retry_test.go b/retry_test.go index 8d58cc16..84c12a48 100644 --- a/retry_test.go +++ b/retry_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/shellescape/shellescape.go b/shellescape/shellescape.go new file mode 100644 index 00000000..5e6a3799 --- /dev/null +++ b/shellescape/shellescape.go @@ -0,0 +1,34 @@ +/* +Package shellescape provides the shellescape.Quote to escape arbitrary +strings for a safe use as command line arguments in the most common +POSIX shells. + +The original Python package which this work was inspired by can be found +at https://pypi.python.org/pypi/shellescape. +*/ +package shellescape + +import ( + "regexp" + "strings" +) + +var pattern *regexp.Regexp + +func init() { + pattern = regexp.MustCompile(`[^\w@%+=:,./-]`) +} + +// Quote returns a shell-escaped version of the string s. The returned value +// is a string that can safely be used as one token in a shell command line. +func Quote(s string) string { + if len(s) == 0 { + return "''" + } + + if pattern.MatchString(s) { + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" + } + + return s +} diff --git a/trace.go b/trace.go index be7555c2..7798a395 100644 --- a/trace.go +++ b/trace.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/transport.go b/transport.go index 191cd519..13c3de34 100644 --- a/transport.go +++ b/transport.go @@ -1,7 +1,7 @@ //go:build go1.13 // +build go1.13 -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/transport112.go b/transport112.go index d4aa4175..beb0301a 100644 --- a/transport112.go +++ b/transport112.go @@ -1,7 +1,7 @@ //go:build !go1.13 // +build !go1.13 -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/util.go b/util.go index 5a69e4fc..7bbba912 100644 --- a/util.go +++ b/util.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file. diff --git a/util_curl.go b/util_curl.go new file mode 100644 index 00000000..e50c3b3a --- /dev/null +++ b/util_curl.go @@ -0,0 +1,76 @@ +package resty + +import ( + "bytes" + "io" + "net/http" + "net/http/cookiejar" + + "net/url" + "strings" + + "github.com/go-resty/resty/v2/shellescape" +) + +func buildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) { + // 1. Generate curl raw headers + curl = "curl -X " + req.Method + " " + // req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " " + headers := dumpCurlHeaders(req) + for _, kv := range *headers { + curl += `-H ` + shellescape.Quote(kv[0]+": "+kv[1]) + ` ` + } + + // 2. Generate curl cookies + if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok { + cookies := cookieJar.Cookies(req.URL) + if len(cookies) > 0 { + curl += ` -H ` + shellescape.Quote(dumpCurlCookies(cookies)) + " " + } + } + + // 3. Generate curl body + if req.Body != nil { + buf, _ := io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!! + curl += `-d ` + shellescape.Quote(string(buf)) + } + + urlString := shellescape.Quote(req.URL.String()) + if urlString == "''" { + urlString = "'http://unexecuted-request'" + } + curl += " " + urlString + return curl +} + +// dumpCurlCookies dumps cookies to curl format +func dumpCurlCookies(cookies []*http.Cookie) string { + sb := strings.Builder{} + sb.WriteString("Cookie: ") + for _, cookie := range cookies { + sb.WriteString(cookie.Name + "=" + url.QueryEscape(cookie.Value) + "&") + } + return strings.TrimRight(sb.String(), "&") +} + +// dumpCurlHeaders dumps headers to curl format +func dumpCurlHeaders(req *http.Request) *[][2]string { + headers := [][2]string{} + for k, vs := range req.Header { + for _, v := range vs { + headers = append(headers, [2]string{k, v}) + } + } + n := len(headers) + for i := 0; i < n; i++ { + for j := n - 1; j > i; j-- { + jj := j - 1 + h1, h2 := headers[j], headers[jj] + if h1[0] < h2[0] { + headers[jj], headers[j] = headers[j], headers[jj] + } + } + } + return &headers +} diff --git a/util_test.go b/util_test.go index 74cf4b1f..6c030fd7 100644 --- a/util_test.go +++ b/util_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. +// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. // resty source code and usage is governed by a MIT style // license that can be found in the LICENSE file.