Repository: stripe/stripe-go (go)

Issue:
Encode payloads deterministically
### Is your feature request related to a problem? Please describe.

Instead of mocking complex use-cases, we want to speed up our test pipeline by using https://github.com/google/go-replayers through using a custom `*http.Client`. The problem is now that the payloads are not deterministic with the custom form encoder included in this SDK. Specifically map types, as used for metadata, are not encoded deterministic and therefore mess with the replayer:

```
metadata[interval]=monthly&metadata[plan]=Essentials%400&customer=cus_NKH10bl....
metadata[plan]=Essentials%400&metadata[interval]=monthly&customer=cus_NKH10bl....
```

### Describe the solution you'd like

The payload should be encoded deterministically. This could either be achieved by

1. sorting keys
2. using the stdlib JSON

### Describe alternatives you've considered

1. sorting keys
2. using the stdlib JSON

### Additional context

_No response_

Pull request:
fix: deterministic encoding
Closes #1606 

As described in the issue, currently payloads are not encoded deterministically. This PR is a proposal to fix that.

Patch:
diff --git a/form/form.go b/form/form.go
index 0a96b723..e29e3766 100644
--- a/form/form.go
+++ b/form/form.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net/url"
 	"reflect"
+	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -338,16 +339,24 @@ func isAppender(t reflect.Type) bool {
 }
 
 func mapEncoder(values *Values, v reflect.Value, keyParts []string, _ bool, _ *formOptions) {
+	keys := make([]string, 0, v.Len())
 	for _, keyVal := range v.MapKeys() {
-		if Strict && keyVal.Kind() != reflect.String {
-			panic("Don't support serializing maps with non-string keys")
+		if keyVal.Kind() != reflect.String {
+			if Strict {
+				panic("Don't support serializing maps with non-string keys")
+			}
+			// otherwise keyVal.String() will panic later
+			continue
 		}
-
+		keys = append(keys, keyVal.String())
+	}
+	sort.Strings(keys)
+	for _, key := range keys {
 		// Unlike a property on a struct which will contain a zero value even
 		// if never set, any value found in a map has been explicitly set, so
 		// we always make an effort to encode them, even if a zero value
 		// (that's why we pass through `true` here).
-		reflectValue(values, v.MapIndex(keyVal), true, append(keyParts, keyVal.String()))
+		reflectValue(values, v.MapIndex(reflect.ValueOf(key)), true, append(keyParts, key))
 	}
 }
 


Test patch:
diff --git a/form/form_test.go b/form/form_test.go
index 5e152d97..66c7ecb9 100644
--- a/form/form_test.go
+++ b/form/form_test.go
@@ -379,6 +379,43 @@ func TestAppendToPrefixed(t *testing.T) {
 	assert.Equal(t, []string{"foo"}, form.Get("prefix[string]"))
 }
 
+func TestEncode(t *testing.T) {
+	form := &Values{}
+	form.Add("foo", "bar")
+	form.Add("foo[bar]", "baz")
+	assert.Equal(t, "foo=bar&foo[bar]=baz", form.Encode())
+}
+
+func TestEncodeDeterministically(t *testing.T) {
+	data := map[string]string{
+		"first":  "foo",
+		"second": "bar",
+	}
+	initialForm := &Values{}
+	AppendTo(initialForm, data)
+	encoded := initialForm.Encode()
+
+	for i := 0; i < 100; i++ {
+		form := &Values{}
+		AppendTo(form, data)
+		assert.Equal(t, encoded, form.Encode(), "iteration %d", i)
+	}
+}
+
+func TestEncodeMapNonStringKey(t *testing.T) {
+	// Disable strict mode for this test, so the non-string key is ignored.
+	Strict = false
+	defer func() {
+		Strict = true
+	}()
+
+	form := &Values{}
+	assert.NotPanics(t, func() {
+		AppendTo(form, map[int]string{1: "foo"})
+	})
+	assert.Len(t, form.values, 0)
+}
+
 func TestFormatKey(t *testing.T) {
 	assert.Equal(t, "param", FormatKey([]string{"param"}))
 	assert.Equal(t, "param[key]", FormatKey([]string{"param", "key"}))