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(enhancement)!: seperate generate curl cmd from debug flow #928 #934

Merged
merged 1 commit into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
61 changes: 42 additions & 19 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ type Client struct {
proxyURL *url.URL
requestDebugLog DebugLogCallback
responseDebugLog DebugLogCallback
generateCurlOnDebug bool
generateCurlCmd bool
debugLogCurlCmd bool
unescapeQueryParams bool
loadBalancer LoadBalancer
beforeRequest []RequestMiddleware
Expand Down Expand Up @@ -641,7 +642,8 @@ func (c *Client) R() *Request {
jsonEscapeHTML: c.jsonEscapeHTML,
log: c.log,
setContentLength: c.setContentLength,
generateCurlOnDebug: c.generateCurlOnDebug,
generateCurlCmd: c.generateCurlCmd,
debugLogCurlCmd: c.debugLogCurlCmd,
unescapeQueryParams: c.unescapeQueryParams,
credentials: c.credentials,
}
Expand Down Expand Up @@ -731,7 +733,7 @@ func (c *Client) requestMiddlewares() []RequestMiddleware {
func (c *Client) AddRequestMiddleware(m RequestMiddleware) *Client {
c.lock.Lock()
defer c.lock.Unlock()
idx := len(c.beforeRequest) - 2
idx := len(c.beforeRequest) - 1
c.beforeRequest = slices.Insert(c.beforeRequest, idx, m)
return c
}
Expand Down Expand Up @@ -1965,35 +1967,56 @@ func (c *Client) SetTrace(t bool) *Client {
return c
}

// EnableGenerateCurlOnDebug method enables the generation of CURL commands in the debug log.
// It works in conjunction with debug mode.
// EnableGenerateCurlCmd method enables the generation of curl command at the
// client instance level.
//
// By default, Resty does not log the curl command in the debug log since it has the potential
// to leak sensitive data unless explicitly enabled via [Client.SetDebugLogCurlCmd].
//
// NOTE: Use with care.
// - Potential to leak sensitive data from [Request] and [Response] in the debug log.
// - Beware of memory usage since the request body is reread.
func (c *Client) EnableGenerateCurlOnDebug() *Client {
c.SetGenerateCurlOnDebug(true)
// - Potential to leak sensitive data from [Request] and [Response] in the debug log
// when the debug log option is enabled.
// - Additional memory usage since the request body was reread.
// - curl body is not generated for [io.Reader] and multipart request flow.
func (c *Client) EnableGenerateCurlCmd() *Client {
c.SetGenerateCurlCmd(true)
return c
}

// DisableGenerateCurlOnDebug method disables the option set by [Client.EnableGenerateCurlOnDebug].
func (c *Client) DisableGenerateCurlOnDebug() *Client {
c.SetGenerateCurlOnDebug(false)
// DisableGenerateCurlCmd method disables the option set by [Client.EnableGenerateCurlCmd] or
// [Client.SetGenerateCurlCmd].
func (c *Client) DisableGenerateCurlCmd() *Client {
c.SetGenerateCurlCmd(false)
return c
}

// SetGenerateCurlOnDebug method is used to turn on/off the generate CURL command in debug mode
// at the client instance level. It works in conjunction with debug mode.
// SetGenerateCurlCmd method is used to turn on/off the generate curl command at the
// client instance level.
//
// By default, Resty does not log the curl command in the debug log since it has the potential
// to leak sensitive data unless explicitly enabled via [Client.SetDebugLogCurlCmd].
//
// NOTE: Use with care.
// - Potential to leak sensitive data from [Request] and [Response] in the debug log.
// - Beware of memory usage since the request body is reread.
// - Potential to leak sensitive data from [Request] and [Response] in the debug log
// when the debug log option is enabled.
// - Additional memory usage since the request body was reread.
// - curl body is not generated for [io.Reader] and multipart request flow.
//
// It can be overridden at the request level; see [Request.SetGenerateCurlCmd]
func (c *Client) SetGenerateCurlCmd(b bool) *Client {

Choose a reason for hiding this comment

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

This one shouldn't be exported.

Otherwise, you had no reason to add Enable and Disable one,no?

Or maybe I'm missing something

Copy link
Member Author

Choose a reason for hiding this comment

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

I kept two kinds of usage variation: Resty users choose one approach based on their need/preference. You may see this in nature methods for frequently used ones, e.g. Debug, Trace, curl, and retry default conditions.

c.lock.Lock()
defer c.lock.Unlock()
c.generateCurlCmd = b
return c
}

// SetDebugLogCurlCmd method enables the curl command to be logged in the debug log.
//
// It can be overridden at the request level; see [Request.SetGenerateCurlOnDebug]
func (c *Client) SetGenerateCurlOnDebug(b bool) *Client {
// It can be overridden at the request level; see [Request.SetDebugLogCurlCmd]
func (c *Client) SetDebugLogCurlCmd(b bool) *Client {
c.lock.Lock()
defer c.lock.Unlock()
c.generateCurlOnDebug = b
c.debugLogCurlCmd = b
return c
}

Expand Down
6 changes: 3 additions & 3 deletions curl.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ import (
)

func buildCurlCmd(req *Request) string {
// 1. Generate curl raw headers
// generate curl raw headers
var curl = "curl -X " + req.Method + " "
headers := dumpCurlHeaders(req.RawRequest)
for _, kv := range *headers {
curl += "-H " + cmdQuote(kv[0]+": "+kv[1]) + " "
}

// 2. Generate curl cookies
// generate curl cookies
if cookieJar := req.client.CookieJar(); cookieJar != nil {
if cookies := cookieJar.Cookies(req.RawRequest.URL); len(cookies) > 0 {
curl += "-H " + cmdQuote(dumpCurlCookies(cookies)) + " "
}
}

// 3. Generate curl body except for io.Reader and multipart request
// generate curl body except for io.Reader and multipart request flow
if req.RawRequest.GetBody != nil {
body, err := req.RawRequest.GetBody()
if err == nil {
Expand Down
33 changes: 18 additions & 15 deletions curl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ func TestCurlGenerateUnexecutedRequest(t *testing.T) {
).
SetMethod(MethodPost)

assertEqual(t, "", req.GenerateCurlCommand())
assertEqual(t, "", req.CurlCmd())

curlCmdUnexecuted := req.EnableGenerateCurlOnDebug().GenerateCurlCommand()
req.DisableGenerateCurlOnDebug()
curlCmdUnexecuted := req.EnableGenerateCurlCmd().CurlCmd()
req.DisableGenerateCurlCmd()

if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") ||
!strings.Contains(curlCmdUnexecuted, "curl -X POST") ||
Expand Down Expand Up @@ -60,15 +60,15 @@ func TestCurlGenerateExecutedRequest(t *testing.T) {

url := ts.URL + "/curl-cmd-post"
resp, err := req.
EnableGenerateCurlOnDebug().
EnableGenerateCurlCmd().
Post(url)
if err != nil {
t.Fatal(err)
}
curlCmdExecuted := resp.Request.GenerateCurlCommand()
curlCmdExecuted := resp.Request.CurlCmd()

c.DisableGenerateCurlOnDebug()
req.DisableGenerateCurlOnDebug()
c.DisableGenerateCurlCmd()
req.DisableGenerateCurlCmd()
if !strings.Contains(curlCmdExecuted, "Cookie: count=1") ||
!strings.Contains(curlCmdExecuted, "curl -X POST") ||
!strings.Contains(curlCmdExecuted, `-d '{"name":"Resty"}'`) ||
Expand All @@ -84,17 +84,20 @@ func TestCurlCmdDebugMode(t *testing.T) {
defer ts.Close()

c, logBuf := dcldb()
c.EnableGenerateCurlCmd().
SetDebugLogCurlCmd(true)

// Build request
req := c.EnableGenerateCurlOnDebug().R().
req := c.R().
SetBody(map[string]string{
"name": "Resty",
}).
SetCookies(
[]*http.Cookie{
{Name: "count", Value: "1"},
},
)
).
SetDebugLogCurlCmd(true)

// Execute request: set debug mode
url := ts.URL + "/curl-cmd-post"
Expand All @@ -103,8 +106,8 @@ func TestCurlCmdDebugMode(t *testing.T) {
t.Fatal(err)
}

c.DisableGenerateCurlOnDebug()
req.DisableGenerateCurlOnDebug()
c.DisableGenerateCurlCmd()
req.DisableGenerateCurlCmd()

// test logContent curl cmd
logContent := logBuf.String()
Expand Down Expand Up @@ -237,10 +240,10 @@ func TestCurlRequestGetBodyError(t *testing.T) {
).
SetMethod(MethodPost)

assertEqual(t, "", req.GenerateCurlCommand())
assertEqual(t, "", req.CurlCmd())

curlCmdUnexecuted := req.EnableGenerateCurlOnDebug().GenerateCurlCommand()
req.DisableGenerateCurlOnDebug()
curlCmdUnexecuted := req.EnableGenerateCurlCmd().CurlCmd()
req.DisableGenerateCurlCmd()

if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") ||
!strings.Contains(curlCmdUnexecuted, "curl -X POST") ||
Expand All @@ -261,7 +264,7 @@ func TestCurlRequestMiddlewaresError(t *testing.T) {
PrepareRequestMiddleware,
)

curlCmdUnexecuted := c.R().EnableGenerateCurlOnDebug().GenerateCurlCommand()
curlCmdUnexecuted := c.R().EnableGenerateCurlCmd().CurlCmd()
assertEqual(t, "", curlCmdUnexecuted)
}

Expand Down
16 changes: 3 additions & 13 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,10 @@ func PrepareRequestMiddleware(c *Client, r *Request) (err error) {
// is URL-related, and those get caught up in the `parseRequestURL`
createRawRequest(c, r)

// last one doesn't need if condition
return addCredentials(c, r)
}
addCredentials(c, r)

_ = r.generateCurlCommand()

// GenerateCurlRequestMiddleware method is used to perform CURL command
// generation during a request preparation
//
// See, [Client.SetGenerateCurlOnDebug], [Request.SetGenerateCurlOnDebug]
func GenerateCurlRequestMiddleware(c *Client, r *Request) (err error) {
if r.Debug && r.generateCurlOnDebug {
if isStringEmpty(r.resultCurlCmd) {
r.resultCurlCmd = buildCurlCmd(r)
}
}
return nil
}

Expand Down
97 changes: 61 additions & 36 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,29 +95,12 @@ type Request struct {
multipartFields []*MultipartField
retryConditions []RetryConditionFunc
resultCurlCmd string
generateCurlOnDebug bool
generateCurlCmd bool
debugLogCurlCmd bool
unescapeQueryParams bool
multipartErrChan chan error
}

// GenerateCurlCommand method generates the CURL command for the request.
func (r *Request) GenerateCurlCommand() string {
if !(r.Debug && r.generateCurlOnDebug) {
return ""
}
if len(r.resultCurlCmd) > 0 {
return r.resultCurlCmd
}
if r.RawRequest == nil {
if err := r.client.executeRequestMiddlewares(r); err != nil {
r.log.Errorf("%v", err)
return ""
}
}
r.resultCurlCmd = buildCurlCmd(r)
return r.resultCurlCmd
}

// SetMethod method used to set the HTTP verb for the request
func (r *Request) SetMethod(m string) *Request {
r.Method = m
Expand Down Expand Up @@ -1086,37 +1069,79 @@ func (r *Request) SetTrace(t bool) *Request {
return r
}

// EnableGenerateCurlOnDebug method enables the generation of CURL commands in the debug log.
// It works in conjunction with debug mode. It overrides the options set by the [Client].
// EnableGenerateCurlCmd method enables the generation of curl commands for the current request.
// It overrides the options set in the [Client].
//
// By default, Resty does not log the curl command in the debug log since it has the potential
// to leak sensitive data unless explicitly enabled via [Request.SetDebugLogCurlCmd].
//
// NOTE: Use with care.
// - Potential to leak sensitive data from [Request] and [Response] in the debug log.
// - Beware of memory usage since the request body is reread.
func (r *Request) EnableGenerateCurlOnDebug() *Request {
r.SetGenerateCurlOnDebug(true)
// - Potential to leak sensitive data from [Request] and [Response] in the debug log
// when the debug log option is enabled.
// - Additional memory usage since the request body was reread.
// - curl body is not generated for [io.Reader] and multipart request flow.
func (r *Request) EnableGenerateCurlCmd() *Request {
r.SetGenerateCurlCmd(true)
return r
}

// DisableGenerateCurlOnDebug method disables the option set by [Request.EnableGenerateCurlOnDebug].
// It overrides the options set by the [Client].
func (r *Request) DisableGenerateCurlOnDebug() *Request {
r.SetGenerateCurlOnDebug(false)
// DisableGenerateCurlCmd method disables the option set by [Request.EnableGenerateCurlCmd] or
// [Request.SetGenerateCurlCmd].
//
// It overrides the options set in the [Client].
func (r *Request) DisableGenerateCurlCmd() *Request {
r.SetGenerateCurlCmd(false)
return r
}

// SetGenerateCurlOnDebug method is used to turn on/off the generate CURL command in debug mode.
// It works in conjunction with debug mode.
// SetGenerateCurlCmd method is used to turn on/off the generate curl command for the current request.
//
// By default, Resty does not log the curl command in the debug log since it has the potential
// to leak sensitive data unless explicitly enabled via [Request.SetDebugLogCurlCmd].
//
// NOTE: Use with care.
// - Potential to leak sensitive data from [Request] and [Response] in the debug log.
// - Beware of memory usage since the request body is reread.
// - Potential to leak sensitive data from [Request] and [Response] in the debug log
// when the debug log option is enabled.
// - Additional memory usage since the request body was reread.
// - curl body is not generated for [io.Reader] and multipart request flow.
//
// It overrides the options set by the [Client.SetGenerateCurlOnDebug]
func (r *Request) SetGenerateCurlOnDebug(b bool) *Request {
r.generateCurlOnDebug = b
// It overrides the options set by the [Client.SetGenerateCurlCmd]
func (r *Request) SetGenerateCurlCmd(b bool) *Request {
r.generateCurlCmd = b
return r
}

// SetDebugLogCurlCmd method enables the curl command to be logged in the debug log
// for the current request.
//
// It can be overridden at the request level; see [Client.SetDebugLogCurlCmd]
func (r *Request) SetDebugLogCurlCmd(b bool) *Request {
r.debugLogCurlCmd = b
return r
}

// CurlCmd method generates the curl command for the request.
func (r *Request) CurlCmd() string {
return r.generateCurlCommand()
}

func (r *Request) generateCurlCommand() string {
if !r.generateCurlCmd {
return ""
}
if len(r.resultCurlCmd) > 0 {
return r.resultCurlCmd
}
if r.RawRequest == nil {
if err := r.client.executeRequestMiddlewares(r); err != nil {
r.log.Errorf("%v", err)
return ""
}
}
r.resultCurlCmd = buildCurlCmd(r)
return r.resultCurlCmd
}

// SetUnescapeQueryParams method sets the choice of unescape query parameters for the request URL.
// To prevent broken URL, Resty replaces space (" ") with "+" in the query parameters.
//
Expand Down
1 change: 0 additions & 1 deletion resty.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ func createClient(hc *http.Client) *Client {
// request middlewares
c.SetRequestMiddlewares(
PrepareRequestMiddleware,
GenerateCurlRequestMiddleware,
)

// response middlewares
Expand Down
2 changes: 1 addition & 1 deletion util.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func requestDebugLogger(c *Client, r *Request) {

reqLog := "\n==============================================================================\n"

if r.Debug && r.generateCurlOnDebug {
if r.generateCurlCmd && r.debugLogCurlCmd {
reqLog += "~~~ REQUEST(CURL) ~~~\n" +
fmt.Sprintf(" %v\n", r.resultCurlCmd)
}
Expand Down
Loading