Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(curl): generate curl cmd for request && example for curl cmd #794

Merged
merged 3 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ _testmain.go
coverage.out
coverage.txt

# Exclude intellij IDE folders
# Exclude IDE folders
.idea/*
.vscode/*
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:")
Expand Down Expand Up @@ -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 : <nil>
Status Code: 200
Expand Down
24 changes: 16 additions & 8 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,9 +1148,7 @@
// 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()
Expand All @@ -1166,22 +1164,22 @@
// 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)
}
}

// If there is a rate limiter set for this client, the Execute call
// 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)
}
}

Expand All @@ -1192,15 +1190,24 @@
// 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)

Check warning on line 1193 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L1193

Added line #L1193 was not covered by tests
}
}

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)
Expand Down Expand Up @@ -1396,6 +1403,7 @@
parseRequestBody,
createHTTPRequest,
addCredentials,
createCurlCmd,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ahuigo Please remove this middleware.

}

// user defined request middlewares
Expand Down
126 changes: 126 additions & 0 deletions examples/debug_curl_test.go
Original file line number Diff line number Diff line change
@@ -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.Stdout
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
}
162 changes: 162 additions & 0 deletions examples/server_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading