Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: go-resty/resty
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v2.15.3
Choose a base ref
...
head repository: go-resty/resty
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v2.16.0
Choose a head ref
  • 6 commits
  • 10 files changed
  • 3 contributors

Commits on Oct 10, 2024

  1. feat(enhancement): improve buildCurlRequest for unit test (#884)

    Co-authored-by: yaziedda <yazied_uddien@linkaja.id>
    yaziedda and yaziedda-la authored Oct 10, 2024
    Copy the full SHA
    fc51b33 View commit details
  2. feat: option to enable URL query params without encoding (#885)

    jeevatkm authored Oct 10, 2024
    Copy the full SHA
    a6a489b View commit details

Commits on Oct 26, 2024

  1. Copy the full SHA
    94aeffb View commit details
  2. build: reduce test execution time

    jeevatkm committed Oct 26, 2024
    Copy the full SHA
    cf921ad View commit details
  3. Merge pull request #892 from go-resty/code-optimizations

    jeevatkm authored Oct 26, 2024
    Copy the full SHA
    1c4960b View commit details

Commits on Nov 11, 2024

  1. release: version bump and readme update for v2.16.0 (#907)

    jeevatkm authored Nov 11, 2024
    Copy the full SHA
    feedf18 View commit details
Showing with 224 additions and 29 deletions.
  1. +3 −3 README.md
  2. +13 −0 client.go
  3. +111 −0 curl_cmd_test.go
  4. +19 −8 middleware.go
  5. +40 −0 middleware_test.go
  6. +12 −0 request.go
  7. +1 −1 resty.go
  8. +8 −0 resty_test.go
  9. +14 −14 retry_test.go
  10. +3 −3 util_curl.go
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -4,12 +4,12 @@
<p align="center"><a href="#features">Features</a> section describes in detail about Resty capabilities</p>
</p>
<p align="center">
<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Av2"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg?branch=v2" alt="Build Status"></a> <a href="https://app.codecov.io/gh/go-resty/resty/tree/v2"><img src="https://codecov.io/gh/go-resty/resty/branch/v2/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.15.3-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Av2"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg?branch=v2" alt="Build Status"></a> <a href="https://app.codecov.io/gh/go-resty/resty/tree/v2"><img src="https://codecov.io/gh/go-resty/resty/branch/v2/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.16.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
</p>

## News

* v2.15.3 [released](https://github.com/go-resty/resty/releases/tag/v2.15.3) and tagged on Sep 26, 2024.
* v2.16.0 [released](https://github.com/go-resty/resty/releases/tag/v2.16.0) and tagged on Nov 10, 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).
@@ -105,7 +105,7 @@ Resty author also published following projects for Go Community.

```bash
# Go Modules
require github.com/go-resty/resty/v2 v2.15.3
require github.com/go-resty/resty/v2 v2.16.0
```

## Usage
13 changes: 13 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -155,6 +155,7 @@ type Client struct {
panicHooks []ErrorHook
rateLimiter RateLimiter
generateCurlOnDebug bool
unescapeQueryParams bool
}

// User type is to hold an username and password information
@@ -325,6 +326,17 @@ func (c *Client) SetQueryParams(params map[string]string) *Client {
return c
}

// SetUnescapeQueryParams method sets the unescape query parameters choice for request URL.
// To prevent broken URL, resty replaces space (" ") with "+" in the query parameters.
//
// See [Request.SetUnescapeQueryParams]
//
// NOTE: Request failure is possible due to non-standard usage of Unescaped Query Parameters.
func (c *Client) SetUnescapeQueryParams(unescape bool) *Client {
c.unescapeQueryParams = unescape
return c
}

// SetFormData method sets Form parameters and their values in the client instance.
// It applies only to HTTP methods `POST` and `PUT`, and the request content type would be set as
// `application/x-www-form-urlencoded`. These form data will be added to all the requests raised from
@@ -446,6 +458,7 @@ func (c *Client) R() *Request {
log: c.log,
responseBodyLimit: c.ResponseBodyLimit,
generateCurlOnDebug: c.generateCurlOnDebug,
unescapeQueryParams: c.unescapeQueryParams,
}
return r
}
111 changes: 111 additions & 0 deletions curl_cmd_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package resty

import (
"bytes"
"io"
"net/http"
"net/http/cookiejar"
"os"
"strings"
"testing"
@@ -134,3 +136,112 @@ func captureStderr() (getOutput func() string, restore func()) {
}
return getOutput, restore
}

func TestBuildCurlCommand(t *testing.T) {
tests := []struct {
name string
method string
url string
headers map[string]string
body string
cookies []*http.Cookie
expected string
}{
{
name: "With Headers",
method: "GET",
url: "http://example.com",
headers: map[string]string{"Content-Type": "application/json", "Authorization": "Bearer token"},
expected: "curl -X GET -H 'Authorization: Bearer token' -H 'Content-Type: application/json' http://example.com",
},
{
name: "With Body",
method: "POST",
url: "http://example.com",
headers: map[string]string{"Content-Type": "application/json"},
body: `{"key":"value"}`,
expected: "curl -X POST -H 'Content-Type: application/json' -d '{\"key\":\"value\"}' http://example.com",
},
{
name: "With Empty Body",
method: "POST",
url: "http://example.com",
headers: map[string]string{"Content-Type": "application/json"},
expected: "curl -X POST -H 'Content-Type: application/json' http://example.com",
},
{
name: "With Query Params",
method: "GET",
url: "http://example.com?param1=value1&param2=value2",
expected: "curl -X GET 'http://example.com?param1=value1&param2=value2'",
},
{
name: "With Special Characters in URL",
method: "GET",
url: "http://example.com/path with spaces",
expected: "curl -X GET http://example.com/path%20with%20spaces",
},
{
name: "With Cookies",
method: "GET",
url: "http://example.com",
cookies: []*http.Cookie{{Name: "session_id", Value: "abc123"}},
expected: "curl -X GET -H 'Cookie: session_id=abc123' http://example.com",
},
{
name: "Without Cookies",
method: "GET",
url: "http://example.com",
expected: "curl -X GET http://example.com",
},
{
name: "With Multiple Cookies",
method: "GET",
url: "http://example.com",
cookies: []*http.Cookie{{Name: "session_id", Value: "abc123"}, {Name: "user_id", Value: "user456"}},
expected: "curl -X GET -H 'Cookie: session_id=abc123&user_id=user456' http://example.com",
},
{
name: "With Empty Cookie Jar",
method: "GET",
url: "http://example.com",
expected: "curl -X GET http://example.com",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup request
var (
req *http.Request
err error
)

if tt.body != "" {
req, err = http.NewRequest(tt.method, tt.url, bytes.NewBufferString(tt.body))
} else {
req, err = http.NewRequest(tt.method, tt.url, nil)
}

if err != nil {
t.Fatalf("failed to create request: %v", err)
}

for k, v := range tt.headers {
req.Header.Set(k, v)
}

// Setup cookie jar
cookieJar, _ := cookiejar.New(nil)
if len(tt.cookies) > 0 {
cookieJar.SetCookies(req.URL, tt.cookies)
}

// Generate curl command
curl := buildCurlRequest(req, cookieJar)

// Assert
assertEqual(t, tt.expected, curl)
})
}
}
27 changes: 19 additions & 8 deletions middleware.go
Original file line number Diff line number Diff line change
@@ -154,6 +154,15 @@ func parseRequestURL(c *Client, r *Request) error {
}
}

// GH#797 Unescape query parameters
if r.unescapeQueryParams && len(reqURL.RawQuery) > 0 {
// at this point, all errors caught up in the above operations
// so ignore the return error on query unescape; I realized
// while writing the unit test
unescapedQuery, _ := url.QueryUnescape(reqURL.RawQuery)
reqURL.RawQuery = strings.ReplaceAll(unescapedQuery, " ", "+") // otherwise request becomes bad request
}

r.URL = reqURL.String()

return nil
@@ -254,17 +263,19 @@ func createHTTPRequest(c *Client, r *Request) (err error) {
r.RawRequest = r.RawRequest.WithContext(r.ctx)
}

bodyCopy, err := getBodyCopy(r)
if err != nil {
return err
}

// assign get body func for the underlying raw request instance
r.RawRequest.GetBody = func() (io.ReadCloser, error) {
if r.RawRequest.GetBody == nil {
bodyCopy, err := getBodyCopy(r)
if err != nil {
return err
}
if bodyCopy != nil {
return io.NopCloser(bytes.NewReader(bodyCopy.Bytes())), nil
buf := bodyCopy.Bytes()
r.RawRequest.GetBody = func() (io.ReadCloser, error) {
b := bytes.NewReader(buf)
return io.NopCloser(b), nil
}
}
return nil, nil
}

return
40 changes: 40 additions & 0 deletions middleware_test.go
Original file line number Diff line number Diff line change
@@ -265,6 +265,23 @@ func Test_parseRequestURL(t *testing.T) {
},
expectedURL: "https://example.com/?foo=1&foo=2",
},
{
name: "unescape query params",
init: func(c *Client, r *Request) {
c.SetBaseURL("https://example.com/").
SetUnescapeQueryParams(true). // this line is just code coverage; I will restructure this test in v3 for the client and request the respective init method
SetQueryParam("fromclient", "hey unescape").
SetQueryParam("initone", "cáfe")

r.SetUnescapeQueryParams(true) // this line takes effect
r.SetQueryParams(
map[string]string{
"registry": "nacos://test:6801", // GH #797
},
)
},
expectedURL: "https://example.com?initone=cáfe&fromclient=hey+unescape&registry=nacos://test:6801",
},
} {
t.Run(tt.name, func(t *testing.T) {
c := New()
@@ -292,6 +309,29 @@ func Test_parseRequestURL(t *testing.T) {
}
}

func TestRequestURL_GH797(t *testing.T) {
ts := createGetServer(t)
defer ts.Close()

c := dc().
SetBaseURL(ts.URL).
SetUnescapeQueryParams(true). // this line is just code coverage; I will restructure this test in v3 for the client and request the respective init method
SetQueryParam("fromclient", "hey unescape").
SetQueryParam("initone", "cáfe")

resp, err := c.R().
SetUnescapeQueryParams(true). // this line takes effect
SetQueryParams(
map[string]string{
"registry": "nacos://test:6801", // GH #797
},
).
Get("/unescape-query-params")

assertError(t, err)
assertEqual(t, "query params looks good", resp.String())
}

func Benchmark_parseRequestURL_PathParams(b *testing.B) {
c := New().SetPathParams(map[string]string{
"foo": "1",
12 changes: 12 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ type Request struct {
retryConditions []RetryConditionFunc
responseBodyLimit int
generateCurlOnDebug bool
unescapeQueryParams bool
}

// GenerateCurlCommand method generates the CURL command for the request.
@@ -210,6 +211,17 @@ func (r *Request) SetQueryParams(params map[string]string) *Request {
return r
}

// SetUnescapeQueryParams method sets the unescape query parameters choice for request URL.
// To prevent broken URL, resty replaces space (" ") with "+" in the query parameters.
//
// This method overrides the value set by [Client.SetUnescapeQueryParams]
//
// NOTE: Request failure is possible due to non-standard usage of Unescaped Query Parameters.
func (r *Request) SetUnescapeQueryParams(unescape bool) *Request {
r.unescapeQueryParams = unescape
return r
}

// SetQueryParamsFromValues method appends multiple parameters with multi-value
// ([url.Values]) at one go in the current request. It will be formed as
// query string for the request.
2 changes: 1 addition & 1 deletion resty.go
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import (
)

// Version # of resty
const Version = "2.15.3"
const Version = "2.16.0"

// New method creates a new Resty client.
func New() *Client {
8 changes: 8 additions & 0 deletions resty_test.go
Original file line number Diff line number Diff line change
@@ -121,6 +121,14 @@ func createGetServer(t *testing.T) *httptest.Server {
case "/not-found-no-error":
w.Header().Set(hdrContentTypeKey, "application/json")
w.WriteHeader(http.StatusNotFound)
case "/unescape-query-params":
initOne := r.URL.Query().Get("initone")
fromClient := r.URL.Query().Get("fromclient")
registry := r.URL.Query().Get("registry")
assertEqual(t, "cáfe", initOne)
assertEqual(t, "hey unescape", fromClient)
assertEqual(t, "nacos://test:6801", registry)
_, _ = w.Write([]byte(`query params looks good`))
}

switch {
28 changes: 14 additions & 14 deletions retry_test.go
Original file line number Diff line number Diff line change
@@ -219,8 +219,8 @@ func TestClientRetryWait(t *testing.T) {
retryIntervals := make([]uint64, retryCount+1)

// Set retry wait times that do not intersect with default ones
retryWaitTime := time.Duration(3) * time.Second
retryMaxWaitTime := time.Duration(9) * time.Second
retryWaitTime := time.Duration(50) * time.Millisecond
retryMaxWaitTime := time.Duration(150) * time.Millisecond

c := dc().
SetRetryCount(retryCount).
@@ -262,7 +262,7 @@ func TestClientRetryWaitMaxInfinite(t *testing.T) {
retryIntervals := make([]uint64, retryCount+1)

// Set retry wait times that do not intersect with default ones
retryWaitTime := time.Duration(3) * time.Second
retryWaitTime := time.Duration(100) * time.Millisecond
retryMaxWaitTime := time.Duration(-1.0) // negative value

c := dc().
@@ -319,8 +319,8 @@ func TestClientRetryWaitCallbackError(t *testing.T) {
retryIntervals := make([]uint64, retryCount+1)

// Set retry wait times that do not intersect with default ones
retryWaitTime := 3 * time.Second
retryMaxWaitTime := 9 * time.Second
retryWaitTime := 50 * time.Millisecond
retryMaxWaitTime := 150 * time.Millisecond

retryAfter := func(client *Client, resp *Response) (time.Duration, error) {
return 0, errors.New("quota exceeded")
@@ -359,11 +359,11 @@ func TestClientRetryWaitCallback(t *testing.T) {
retryIntervals := make([]uint64, retryCount+1)

// Set retry wait times that do not intersect with default ones
retryWaitTime := 3 * time.Second
retryMaxWaitTime := 9 * time.Second
retryWaitTime := 50 * time.Millisecond
retryMaxWaitTime := 150 * time.Millisecond

retryAfter := func(client *Client, resp *Response) (time.Duration, error) {
return 5 * time.Second, nil
return 50 * time.Millisecond, nil
}

c := dc().
@@ -407,11 +407,11 @@ func TestClientRetryWaitCallbackTooShort(t *testing.T) {
retryIntervals := make([]uint64, retryCount+1)

// Set retry wait times that do not intersect with default ones
retryWaitTime := 3 * time.Second
retryMaxWaitTime := 9 * time.Second
retryWaitTime := 50 * time.Millisecond
retryMaxWaitTime := 150 * time.Millisecond

retryAfter := func(client *Client, resp *Response) (time.Duration, error) {
return 2 * time.Second, nil // too short duration
return 10 * time.Millisecond, nil // too short duration
}

c := dc().
@@ -455,11 +455,11 @@ func TestClientRetryWaitCallbackTooLong(t *testing.T) {
retryIntervals := make([]uint64, retryCount+1)

// Set retry wait times that do not intersect with default ones
retryWaitTime := 1 * time.Second
retryMaxWaitTime := 3 * time.Second
retryWaitTime := 10 * time.Millisecond
retryMaxWaitTime := 100 * time.Millisecond

retryAfter := func(client *Client, resp *Response) (time.Duration, error) {
return 4 * time.Second, nil // too long duration
return 150 * time.Millisecond, nil // too long duration
}

c := dc().
6 changes: 3 additions & 3 deletions util_curl.go
Original file line number Diff line number Diff line change
@@ -27,22 +27,22 @@ func buildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl str
if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok {
cookies := cookieJar.Cookies(req.URL)
if len(cookies) > 0 {
curl += ` -H ` + shellescape.Quote(dumpCurlCookies(cookies)) + " "
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))
curl += `-d ` + shellescape.Quote(string(buf)) + " "
}

urlString := shellescape.Quote(req.URL.String())
if urlString == "''" {
urlString = "'http://unexecuted-request'"
}
curl += " " + urlString
curl += urlString
return curl
}