diff --git a/bind.go b/bind.go index bec2e3b3a..676fab836 100644 --- a/bind.go +++ b/bind.go @@ -241,12 +241,15 @@ func bindData(destination any, data map[string][]string, tag string, dataFiles m val.Set(reflect.MakeMap(typ)) } for k, v := range data { - if isElemString { - val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) - } else if isElemInterface { - // To maintain backward compatibility, we always bind to the first string value - // and not the slice of strings when dealing with map[string]any{} - val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) + if isElemString || isElemInterface { + // To maintain backward compatibility, map[string]any binds the first + // string, not the slice. An empty value slice is a blank string. + // Indexing it would panic, and net/http treats that slice as empty. + value := "" + if len(v) > 0 { + value = v[0] + } + val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(value)) } else { val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v)) } @@ -334,6 +337,12 @@ func bindData(destination any, data map[string][]string, tag string, dataFiles m continue } + // A present key with no values is a blank header or parameter. + // net/http Header.Get returns "" for that slice. Do not index it. + if len(inputValue) == 0 { + continue + } + if ok, err := unmarshalInputToField(fm.fieldKind, inputValue[0], structField, fm.formatTag); ok { if err != nil { return fmt.Errorf("%s: %w", inputFieldName, err) diff --git a/bind_test.go b/bind_test.go index 771c59fb8..8d0c70e7c 100644 --- a/bind_test.go +++ b/bind_test.go @@ -290,6 +290,39 @@ func TestBindHeaderParam(t *testing.T) { } } +func TestBindHeadersEmptyValueSlice(t *testing.T) { + e := New() + for _, values := range [][]string{nil, {}} { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header["X-Empty"] = values + req.Header.Set("Name", "Jon Doe") + c := e.NewContext(req, httptest.NewRecorder()) + + dst := struct { + Value string `header:"X-Empty"` + Name string `header:"Name"` + }{} + err := BindHeaders(c, &dst) + if assert.NoError(t, err) { + assert.Equal(t, "", dst.Value) + assert.Equal(t, "Jon Doe", dst.Name) + } + + strMap := map[string]string{} + err = BindHeaders(c, &strMap) + if assert.NoError(t, err) { + assert.Equal(t, "", strMap["X-Empty"]) + assert.Equal(t, "Jon Doe", strMap["Name"]) + } + + anyMap := map[string]any{} + err = BindHeaders(c, &anyMap) + if assert.NoError(t, err) { + assert.Equal(t, "", anyMap["X-Empty"]) + } + } +} + func TestBindHeaderParamBadType(t *testing.T) { e := New() req := httptest.NewRequest(http.MethodGet, "/", nil)