From 08879b9e2f802124c9c3d2d463a9556ed3823558 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Wed, 11 Nov 2026 21:22:50 -0800 Subject: [PATCH 1/2] fix(bind): skip empty scalar header values --- bind.go | 16 +++++++++++++++ bind_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/bind.go b/bind.go index bec2e3b3a..17dfdbe88 100644 --- a/bind.go +++ b/bind.go @@ -241,6 +241,11 @@ func bindData(destination any, data map[string][]string, tag string, dataFiles m val.Set(reflect.MakeMap(typ)) } for k, v := range data { + // A scalar destination needs a value. Slice destinations can retain + // empty slices, which is their existing binding behavior. + if len(v) == 0 && !isElemSliceOfStrings { + continue + } if isElemString { val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) } else if isElemInterface { @@ -322,6 +327,17 @@ func bindData(destination any, data map[string][]string, tag string, dataFiles m if !exists { continue } + if len(inputValue) == 0 { + // Preserve custom multi-value unmarshalling, but otherwise treat + // entries without values as absent (without allocating pointers). + _, multiple := structField.Addr().Interface().(bindMultipleUnmarshaler) + if structFieldKind == reflect.Pointer { + _, multiple = structField.Interface().(bindMultipleUnmarshaler) + } + if !multiple { + continue + } + } // NOTE: algorithm here is not particularly sophisticated. It probably does not work with absurd types like `**[]*int` // but it is smart enough to handle niche cases like `*int`,`*[]string`,`[]*int` . diff --git a/bind_test.go b/bind_test.go index 771c59fb8..28e33a8f7 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1873,3 +1873,61 @@ func TestOpenAPIFormatTimeBindingEdgeCases(t *testing.T) { }) } } + +func TestBindHeadersEmptyValues(t *testing.T) { + for _, values := range [][]string{nil, {}, {""}, {"value"}} { + t.Run(fmt.Sprintf("%#v", values), func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header["X-Value"] = values + c := New().NewContext(req, httptest.NewRecorder()) + want := "original" + if len(values) > 0 { + want = values[0] + } + var dst = struct { + Value string `header:"X-Value"` + }{Value: "original"} + assert.NotPanics(t, func() { assert.NoError(t, BindHeaders(c, &dst)) }) + assert.Equal(t, want, dst.Value) + m := map[string]string{"X-Value": "original"} + assert.NotPanics(t, func() { assert.NoError(t, BindHeaders(c, &m)) }) + assert.Equal(t, want, m["X-Value"]) + mi := map[string]any{"X-Value": "original"} + assert.NotPanics(t, func() { assert.NoError(t, BindHeaders(c, &mi)) }) + assert.Equal(t, want, mi["X-Value"]) + ms := map[string][]string{} + assert.NoError(t, BindHeaders(c, &ms)) + assert.Equal(t, values, ms["X-Value"]) + if len(values) == 0 { + var pointer struct { + Value *string `header:"X-Value"` + } + assert.NoError(t, BindHeaders(c, &pointer)) + assert.Nil(t, pointer.Value) + } + }) + } +} + +type headerValuesUnmarshaler struct { + called bool + values []string +} + +func (v *headerValuesUnmarshaler) UnmarshalParams(values []string) error { + v.called, v.values = true, values + return nil +} + +func TestBindHeadersEmptyValuesCustomUnmarshaler(t *testing.T) { + for _, values := range [][]string{nil, {}} { + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header["X-Value"] = values + var dst struct { + Value headerValuesUnmarshaler `header:"X-Value"` + } + assert.NoError(t, BindHeaders(New().NewContext(req, httptest.NewRecorder()), &dst)) + assert.True(t, dst.Value.called) + assert.Equal(t, values, dst.Value.values) + } +} From 4fa636bc9de19f4b796785fa9f5446adae21d837 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Wed, 11 Nov 2026 21:41:21 -0800 Subject: [PATCH 2/2] refactor(bind): simplify empty-value unmarshaler checks --- bind.go | 8 ++++---- bind_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bind.go b/bind.go index 17dfdbe88..0e5ddd469 100644 --- a/bind.go +++ b/bind.go @@ -330,11 +330,11 @@ func bindData(destination any, data map[string][]string, tag string, dataFiles m if len(inputValue) == 0 { // Preserve custom multi-value unmarshalling, but otherwise treat // entries without values as absent (without allocating pointers). - _, multiple := structField.Addr().Interface().(bindMultipleUnmarshaler) - if structFieldKind == reflect.Pointer { - _, multiple = structField.Interface().(bindMultipleUnmarshaler) + value := structField + if structFieldKind != reflect.Pointer { + value = structField.Addr() } - if !multiple { + if _, multiple := value.Interface().(bindMultipleUnmarshaler); !multiple { continue } } diff --git a/bind_test.go b/bind_test.go index 28e33a8f7..c1ef54435 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1929,5 +1929,13 @@ func TestBindHeadersEmptyValuesCustomUnmarshaler(t *testing.T) { assert.NoError(t, BindHeaders(New().NewContext(req, httptest.NewRecorder()), &dst)) assert.True(t, dst.Value.called) assert.Equal(t, values, dst.Value.values) + var pointer struct { + Value *headerValuesUnmarshaler `header:"X-Value"` + } + assert.NoError(t, BindHeaders(New().NewContext(req, httptest.NewRecorder()), &pointer)) + if assert.NotNil(t, pointer.Value) { + assert.True(t, pointer.Value.called) + assert.Equal(t, values, pointer.Value.values) + } } }