From a6a489ba12058a87e4e2ed4aace5190e9184c93d Mon Sep 17 00:00:00 2001 From: "Jeevanandam M." Date: Wed, 9 Oct 2024 23:26:57 -0700 Subject: [PATCH] feat: option to enable URL query params without encoding (#885) --- client.go | 13 +++++++++++++ middleware.go | 9 +++++++++ middleware_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ request.go | 12 ++++++++++++ resty_test.go | 8 ++++++++ 5 files changed, 82 insertions(+) diff --git a/client.go b/client.go index 87a88da2..48c69f19 100644 --- a/client.go +++ b/client.go @@ -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 } diff --git a/middleware.go b/middleware.go index 4f579156..c4ed8d96 100644 --- a/middleware.go +++ b/middleware.go @@ -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 diff --git a/middleware_test.go b/middleware_test.go index 49733e95..d85514e6 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -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®istry=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", diff --git a/request.go b/request.go index 18340715..9075ead5 100644 --- a/request.go +++ b/request.go @@ -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. diff --git a/resty_test.go b/resty_test.go index e10421c1..a148b0a0 100644 --- a/resty_test.go +++ b/resty_test.go @@ -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 {