From 0529b0e07dc6af49348117b0e7d09bf78afcf0b6 Mon Sep 17 00:00:00 2001 From: bintangakbarRK Date: Wed, 5 Aug 2026 21:22:14 +0700 Subject: [PATCH] fix: trim OWS after commas when splitting list parameters RFC 9110 $5.6.1 list grammar permits optional whitespace around each comma, e.g. 'If-None-Match: "a", "b"'. The list-splitting code in parseInto kept the leading space on every element after the first, so conditional.Params ETag comparisons silently missed legitimate matches and served a 200 instead of a 304. --- conditional/params_test.go | 41 ++++++++++++++++++++++++++++++++++++++ huma.go | 7 +++++++ 2 files changed, 48 insertions(+) diff --git a/conditional/params_test.go b/conditional/params_test.go index 7f19d879..dd050399 100644 --- a/conditional/params_test.go +++ b/conditional/params_test.go @@ -1,11 +1,13 @@ package conditional import ( + "context" "net/http" "net/http/httptest" "testing" "time" + "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/humatest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -140,6 +142,45 @@ func TestIfModifiedSince(t *testing.T) { assert.Equal(t, http.StatusPreconditionFailed, perr.GetStatus()) } +func TestIfNoneMatchOWSAfterComma(t *testing.T) { + // RFC 9110 §5.6.1 list grammar permits optional whitespace around each + // comma, e.g. `If-None-Match: "a", "b"`. Every element after the first + // must still match (issue #1084). + _, api := humatest.New(t) + huma.Register(api, huma.Operation{ + OperationID: "conditional", + Method: http.MethodGet, + Path: "/resource", + }, func(ctx context.Context, input *struct { + Params + }) (*struct { + Body string `json:"body"` + }, error) { + if err := input.Params.PreconditionFailed("target-tag", time.Time{}); err != nil { + return nil, err + } + return &struct { + Body string `json:"body"` + }{Body: "ok"}, nil + }) + + // The OWS lands on the second element; without the fix it never matches + // and the request returns 200 instead of 304. + res := api.Get("/resource", `If-None-Match: "other-tag", "target-tag"`) + assert.Equal(t, http.StatusNotModified, res.Code) + + // First-position elements keep working, with and without whitespace. + res = api.Get("/resource", `If-None-Match: "target-tag", "other-tag"`) + assert.Equal(t, http.StatusNotModified, res.Code) + + res = api.Get("/resource", `If-None-Match: "other-tag","target-tag"`) + assert.Equal(t, http.StatusNotModified, res.Code) + + // A non-matching list still serves the resource. + res = api.Get("/resource", `If-None-Match: "other-tag", "yet-another-tag"`) + assert.Equal(t, http.StatusOK, res.Code) +} + func TestIfUnmodifiedSince(t *testing.T) { p := Params{} diff --git a/huma.go b/huma.go index a80bf2c2..1e4ad952 100644 --- a/huma.go +++ b/huma.go @@ -1869,6 +1869,13 @@ func parseInto(ctx Context, f reflect.Value, value string, preSplit []string, p values = (&u).Query()[p.Name] } else { values = strings.Split(value, ",") + // RFC 9110 §5.6.1 list grammar allows optional whitespace + // (OWS) around each comma, e.g. `If-None-Match: "a", "b"`. + // Strip it so later elements are not compared with a leading + // space (e.g. `conditional.Params` ETag matching). + for i, v := range values { + values[i] = strings.Trim(v, " \t") + } } }