[#2763] fixed multipart/form-data array value bind
This commit is contained in:
parent
bd94940eef
commit
9cba6ac386
|
@ -5,9 +5,11 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
|
"github.com/spf13/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BindBody binds request body content to i.
|
// BindBody binds request body content to i.
|
||||||
|
@ -28,10 +30,12 @@ func BindBody(c echo.Context, i interface{}) error {
|
||||||
return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error())
|
return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
default:
|
case strings.HasPrefix(ctype, echo.MIMEApplicationForm), strings.HasPrefix(ctype, echo.MIMEMultipartForm):
|
||||||
// fallback to the default binder
|
return bindFormData(c, i)
|
||||||
return echo.BindBody(c, i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fallback to the default binder
|
||||||
|
return echo.BindBody(c, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyJsonBody reads the request body into i by
|
// CopyJsonBody reads the request body into i by
|
||||||
|
@ -57,3 +61,69 @@ func CopyJsonBody(r *http.Request, i interface{}) error {
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is temp hotfix for properly binding multipart/form-data array values
|
||||||
|
// when a map destination is used.
|
||||||
|
//
|
||||||
|
// It should be replaced with echo.BindBody(c, i) once the issue is fixed in echo.
|
||||||
|
func bindFormData(c echo.Context, i interface{}) error {
|
||||||
|
if i == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
values, err := c.FormValues()
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPErrorWithInternal(http.StatusBadRequest, err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := reflect.TypeOf(i).Elem()
|
||||||
|
|
||||||
|
// map
|
||||||
|
if rt.Kind() == reflect.Map {
|
||||||
|
rv := reflect.ValueOf(i).Elem()
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
if total := len(v); total == 1 {
|
||||||
|
rv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalizeMultipartValue(v[0])))
|
||||||
|
} else {
|
||||||
|
normalized := make([]any, total)
|
||||||
|
for i, vItem := range v {
|
||||||
|
normalized[i] = vItem
|
||||||
|
}
|
||||||
|
rv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(normalized))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// anything else
|
||||||
|
return echo.BindBody(c, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In order to support more seamlessly both json and multipart/form-data requests,
|
||||||
|
// the following normalization rules are applied for plain multipart string values:
|
||||||
|
// - "true" is converted to the json `true`
|
||||||
|
// - "false" is converted to the json `false`
|
||||||
|
// - numeric (non-scientific) strings are converted to json number
|
||||||
|
// - any other string (empty string too) is left as it is
|
||||||
|
func normalizeMultipartValue(raw string) any {
|
||||||
|
switch raw {
|
||||||
|
case "true":
|
||||||
|
return true
|
||||||
|
case "false":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
if raw[0] >= '0' && raw[0] <= '9' {
|
||||||
|
if v, err := cast.ToFloat64E(raw); err == nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package rest_test
|
package rest_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -16,31 +17,40 @@ func TestBindBody(t *testing.T) {
|
||||||
scenarios := []struct {
|
scenarios := []struct {
|
||||||
body io.Reader
|
body io.Reader
|
||||||
contentType string
|
contentType string
|
||||||
result map[string]string
|
expectBody string
|
||||||
expectError bool
|
expectError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
strings.NewReader(""),
|
strings.NewReader(""),
|
||||||
echo.MIMEApplicationJSON,
|
echo.MIMEApplicationJSON,
|
||||||
map[string]string{},
|
`{}`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
strings.NewReader(`{"test":"invalid`),
|
strings.NewReader(`{"test":"invalid`),
|
||||||
echo.MIMEApplicationJSON,
|
echo.MIMEApplicationJSON,
|
||||||
map[string]string{},
|
`{}`,
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
strings.NewReader(`{"test":"test123"}`),
|
strings.NewReader(`{"test":123}`),
|
||||||
echo.MIMEApplicationJSON,
|
echo.MIMEApplicationJSON,
|
||||||
map[string]string{"test": "test123"},
|
`{"test":123}`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
strings.NewReader(url.Values{"test": []string{"test123"}}.Encode()),
|
strings.NewReader(
|
||||||
|
url.Values{
|
||||||
|
"string": []string{"str"},
|
||||||
|
"stings": []string{"str1", "str2"},
|
||||||
|
"number": []string{"123"},
|
||||||
|
"numbers": []string{"123", "456"},
|
||||||
|
"bool": []string{"true"},
|
||||||
|
"bools": []string{"true", "false"},
|
||||||
|
}.Encode(),
|
||||||
|
),
|
||||||
echo.MIMEApplicationForm,
|
echo.MIMEApplicationForm,
|
||||||
map[string]string{"test": "test123"},
|
`{"bool":true,"bools":["true","false"],"number":123,"numbers":["123","456"],"stings":["str1","str2"],"string":"str"}`,
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -52,25 +62,22 @@ func TestBindBody(t *testing.T) {
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
c := e.NewContext(req, rec)
|
c := e.NewContext(req, rec)
|
||||||
|
|
||||||
result := map[string]string{}
|
data := map[string]any{}
|
||||||
err := rest.BindBody(c, &result)
|
err := rest.BindBody(c, &data)
|
||||||
|
|
||||||
if err == nil && scenario.expectError {
|
hasErr := err != nil
|
||||||
t.Errorf("(%d) Expected error, got nil", i)
|
if hasErr != scenario.expectError {
|
||||||
|
t.Errorf("[%d] Expected hasErr %v, got %v", i, scenario.expectError, hasErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil && !scenario.expectError {
|
rawBody, err := json.Marshal(data)
|
||||||
t.Errorf("(%d) Expected nil, got error %v", i, err)
|
if err != nil {
|
||||||
|
t.Errorf("[%d] Failed to marshal binded body: %v", i, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result) != len(scenario.result) {
|
if scenario.expectBody != string(rawBody) {
|
||||||
t.Errorf("(%d) Expected %v, got %v", i, scenario.result, result)
|
t.Errorf("[%d] Expected body \n%s, \ngot \n%s", i, scenario.expectBody, rawBody)
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range result {
|
|
||||||
if sv, ok := scenario.result[k]; !ok || v != sv {
|
|
||||||
t.Errorf("(%d) Expected value %v for key %s, got %v", i, sv, k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue