Skip to content

Commit

Permalink
feat(curl): generate curl cmd for request && example for curl cmd
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuigo committed May 11, 2024
1 parent 877d7e3 commit cbe8748
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 10 deletions.
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 @@ -126,12 +126,17 @@ import "github.com/go-resty/resty/v2"

```go
// Create a Resty Client
var curlCmdExecuted string
client := resty.New()

resp, err := client.R().
SetResultCurlCmd(&curlCmdExecuted).
EnableTrace().
Get("https://httpbin.org/get")

// Explore curl command
fmt.Println("Curl Command:\n ", curlCmdExecuted+"\n")

// Explore response object
fmt.Println("Response Info:")
fmt.Println(" Error :", err)
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 @@ 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()
Expand All @@ -1166,22 +1164,22 @@ 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)
}
}

// 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 @@ 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)
Expand Down Expand Up @@ -1396,6 +1403,7 @@ func createClient(hc *http.Client) *Client {
parseRequestBody,
createHTTPRequest,
addCredentials,
createCurlCmd,
}

// user defined request middlewares
Expand Down
43 changes: 43 additions & 0 deletions examples/debug_curl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package examples

import (
"net/http"
"strings"
"testing"

"github.com/go-resty/resty/v2"
)

// Example about generating curl command
func TestDebugCurl(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", },
},
)

// 1. Generate curl for request(dry-run: request isn't executed)
curlCmdUnexecuted := req.GetCurlCmd()
if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") || !strings.Contains(curlCmdUnexecuted, "curl -X GET") {
t.Fatal("bad curl:", curlCmdUnexecuted)
}else{
t.Log("curlCmdUnexecuted: \n",curlCmdUnexecuted)
}

// 2. Generate curl for request(request is executed)
var curlCmdExecuted string
req.SetResultCurlCmd(&curlCmdExecuted)
if _, err := req.Post(ts.URL+"/post"); err != nil {
t.Fatal(err)
}
if !strings.Contains(curlCmdExecuted, "Cookie: count=1") || !strings.Contains(curlCmdExecuted, "curl -X POST") {
t.Fatal("bad curl:", curlCmdExecuted)
}else{
t.Log("curlCmdExecuted: \n",curlCmdExecuted)
}
}
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
}
11 changes: 10 additions & 1 deletion middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,13 @@ func addCredentials(c *Client, r *Request) error {
return nil
}

func createCurlCmd(c *Client, r *Request) (err error) {
if r.resultCurlCmd!=nil{
*r.resultCurlCmd = BuildCurlRequest(r.RawRequest, c.httpClient.Jar)
}
return nil
}

func requestLogger(c *Client, r *Request) error {
if r.Debug {
rr := r.RawRequest
Expand All @@ -329,12 +336,14 @@ 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) +
fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) +
fmt.Sprintf("BODY :\n%v\n", rl.Body) +
"------------------------------------------------------------------------------\n"
"------------------------------------------------------------------------------\n"

r.initValuesMap()
r.values[debugRequestLogKey] = reqLog
Expand Down
21 changes: 21 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Request struct {
Time time.Time
Body interface{}
Result interface{}
resultCurlCmd *string
Error interface{}
RawRequest *http.Request
SRV *SRVRecord
Expand Down Expand Up @@ -73,6 +74,20 @@ type Request struct {
retryConditions []RetryConditionFunc
}

func (r *Request) GetCurlCmd() string {
// trigger beforeRequest from middleware
if r.RawRequest == nil {
r.client.executeBefore(r) // mock 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 {
Expand Down Expand Up @@ -333,6 +348,12 @@ func (r *Request) SetResult(res interface{}) *Request {
return r
}

// This method is to register curl cmd for request executed.
func (r *Request) SetResultCurlCmd(curlCmd *string) *Request {
r.resultCurlCmd = curlCmd
return r
}

// SetError method is to register the request `Error` object for automatic unmarshalling for the request,
// if response status code is greater than 399 and content type either JSON or XML.
//
Expand Down
Loading

0 comments on commit cbe8748

Please sign in to comment.