Skip to content
Draft
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
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,37 @@ resp, body, errs := request.Get("http://example.com/").
End()
```

## Raw Query String (Preserving Parameter Order)

By default, query parameters are sorted alphabetically when encoded. This can be problematic for APIs that require signature generation based on parameter order.

Use `SetRawQueryString` to send query parameters exactly as provided, preserving their order:

```go
// Parameters will be sent in the exact order: z=last&a=first&m=middle
request := gorequest.New()
resp, body, errs := request.Get("http://example.com/api").
SetRawQueryString("z=last&a=first&m=middle").
End()
```

This is useful when:
- You need to generate signatures based on query parameter order
- The API requires parameters in a specific order
- You want full control over the query string format

Note: When `SetRawQueryString` is set, it takes precedence over `Query()` or `Param()` methods. The raw query string is used as-is without any encoding or reordering.

For JSON body order, you can use `Send()` with a raw JSON string directly:

```go
// JSON will be sent exactly as provided, preserving key order
request := gorequest.New()
resp, body, errs := request.Post("http://example.com/api").
Send(`{"z":"last","a":"first","m":"middle"}`).
End()
```

## Handling Redirects

Redirects can be handled with RedirectPolicy which behaves similarly to
Expand Down
39 changes: 34 additions & 5 deletions gorequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type SuperAgent struct {
SliceData []interface{}
FormData url.Values
QueryData url.Values
RawQueryString string
FileData []File
BounceToRawString bool
RawString string
Expand Down Expand Up @@ -218,6 +219,7 @@ func (s *SuperAgent) Clone() *SuperAgent {
SliceData: shallowCopyDataSlice(s.SliceData),
FormData: url.Values(cloneMapArray(s.FormData)),
QueryData: url.Values(cloneMapArray(s.QueryData)),
RawQueryString: s.RawQueryString,
FileData: shallowCopyFileArray(s.FileData),
BounceToRawString: s.BounceToRawString,
RawString: s.RawString,
Expand Down Expand Up @@ -272,6 +274,7 @@ func (s *SuperAgent) ClearSuperAgent() {
s.SliceData = []interface{}{}
s.FormData = url.Values{}
s.QueryData = url.Values{}
s.RawQueryString = ""
s.FileData = make([]File, 0)
s.BounceToRawString = false
s.RawString = ""
Expand Down Expand Up @@ -596,6 +599,21 @@ func (s *SuperAgent) Param(key string, value string) *SuperAgent {
return s
}

// SetRawQueryString sets a raw query string that will be used as-is without any encoding or reordering.
// This is useful when you need to preserve the exact order of query parameters (e.g., for signature generation).
// Note: When RawQueryString is set, it takes precedence over QueryData.
//
// Example:
//
// gorequest.New().
// Get("http://example.com/api").
// SetRawQueryString("z=last&a=first&m=middle").
// End()
func (s *SuperAgent) SetRawQueryString(rawQueryString string) *SuperAgent {
s.RawQueryString = rawQueryString
return s
}

// Set TLSClientConfig for underling Transport.
// One example is you can use it to disable security check (https):
//
Expand Down Expand Up @@ -1458,13 +1476,24 @@ func (s *SuperAgent) MakeRequest() (*http.Request, error) {
}

// Add all querystring from Query func
q := req.URL.Query()
for k, v := range s.QueryData {
for _, vv := range v {
q.Add(k, vv)
// If RawQueryString is set, use it directly (preserves order)
// Otherwise, use url.Values.Encode() which sorts alphabetically
if s.RawQueryString != "" {
// Use raw query string as-is, preserving order
if req.URL.RawQuery != "" {
req.URL.RawQuery = req.URL.RawQuery + "&" + s.RawQueryString
} else {
req.URL.RawQuery = s.RawQueryString
}
} else {
q := req.URL.Query()
for k, v := range s.QueryData {
for _, vv := range v {
q.Add(k, vv)
}
}
req.URL.RawQuery = q.Encode()
}
req.URL.RawQuery = q.Encode()

// Add basic auth
if s.BasicAuth != struct{ Username, Password string }{} {
Expand Down
79 changes: 79 additions & 0 deletions gorequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,85 @@ func TestQueryFunc(t *testing.T) {
End()
}

// TestSetRawQueryString tests that SetRawQueryString preserves query parameter order
func TestSetRawQueryString(t *testing.T) {
const case1_raw_query = "/raw_query"
const case2_raw_query_with_url_params = "/raw_query_with_url_params"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != GET {
t.Error(fmt.Sprintf("Expected method %q; got %q", GET, r.Method))
}
t.Logf("TestSetRawQueryString: Path=%s, RawQuery=%s", r.URL.Path, r.URL.RawQuery)

switch r.URL.Path {
default:
t.Error(fmt.Sprintf("No testing for this case yet : %q", r.URL.Path))
case case1_raw_query:
// The raw query string should be preserved exactly as provided
expectedRawQuery := "z=last&a=first&m=middle"
if r.URL.RawQuery != expectedRawQuery {
t.Error(fmt.Sprintf("Expected RawQuery=%s | but got %s", expectedRawQuery, r.URL.RawQuery))
}
case case2_raw_query_with_url_params:
// URL already has params, raw query should be appended
expectedRawQuery := "existing=param&z=last&a=first"
if r.URL.RawQuery != expectedRawQuery {
t.Error(fmt.Sprintf("Expected RawQuery=%s | but got %s", expectedRawQuery, r.URL.RawQuery))
}
}
}))
defer ts.Close()

// Test case 1: SetRawQueryString preserves order
New().Get(ts.URL + case1_raw_query).
SetRawQueryString("z=last&a=first&m=middle").
End()

// Test case 2: SetRawQueryString with existing URL parameters
New().Get(ts.URL + case2_raw_query_with_url_params + "?existing=param").
SetRawQueryString("z=last&a=first").
End()
}

// TestSetRawQueryStringClone tests that RawQueryString is properly cloned
func TestSetRawQueryStringClone(t *testing.T) {
const case1_base = "/base"
const case2_override = "/override"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("TestSetRawQueryStringClone: Path=%s, RawQuery=%s", r.URL.Path, r.URL.RawQuery)

switch r.URL.Path {
default:
t.Error(fmt.Sprintf("No testing for this case yet : %q", r.URL.Path))
case case1_base:
expectedRawQuery := "z=last&a=first"
if r.URL.RawQuery != expectedRawQuery {
t.Error(fmt.Sprintf("Expected RawQuery=%s | but got %s", expectedRawQuery, r.URL.RawQuery))
}
case case2_override:
expectedRawQuery := "different=query&order=preserved"
if r.URL.RawQuery != expectedRawQuery {
t.Error(fmt.Sprintf("Expected RawQuery=%s | but got %s", expectedRawQuery, r.URL.RawQuery))
}
}
}))
defer ts.Close()

// Set up base request with a raw query string
baseRequest := New().SetRawQueryString("z=last&a=first")

// Clone 1 should inherit the raw query string
baseRequest.Clone().Get(ts.URL + case1_base).End()

// Clone 2 with override
baseRequest.Clone().Get(ts.URL + case2_override).
SetRawQueryString("different=query&order=preserved").
End()

// Clone 3 should still have the original raw query string
baseRequest.Clone().Get(ts.URL + case1_base).End()
}

// TODO: more tests on redirect
func TestRedirectPolicyFunc(t *testing.T) {
redirectSuccess := false
Expand Down