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
-

+

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.