Skip to content

Commit

Permalink
feat: option to enable URL query params without encoding (#885)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeevatkm authored Oct 10, 2024
1 parent fc51b33 commit a6a489b
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 0 deletions.
13 changes: 13 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -446,6 +458,7 @@ func (c *Client) R() *Request {
log: c.log,
responseBodyLimit: c.ResponseBodyLimit,
generateCurlOnDebug: c.generateCurlOnDebug,
unescapeQueryParams: c.unescapeQueryParams,
}
return r
}
Expand Down
9 changes: 9 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type Request struct {
retryConditions []RetryConditionFunc
responseBodyLimit int
generateCurlOnDebug bool
unescapeQueryParams bool
}

// GenerateCurlCommand method generates the CURL command for the request.
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions resty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit a6a489b

Please sign in to comment.