Repository: swaggest/rest (go)

Issue:
Header names case sensitivity
**Describe the bug**

According to

https://stackoverflow.com/questions/5258977/are-http-headers-case-sensitive

HTTP header names should be compared in a case-insensitive fashion.

With example input header definition

```
HeaderSecFetchSite string `header:"sec-fetch-site" required:"true"`
```

request with

```
Sec-Fetch-Site: same-origin
```
throws request validation failure with

```  
"header:sec-fetch-site": [
      "missing value"
    ]
```

**Expected behavior**
Requests with any of below should be passed:

```
sec-fetch-site: same-origin
Sec-Fetch-Site: same-origin
sEc-fEtCh-sitE: same-origin
```


Pull request description:
Fix check of required headers
Fixes #119.

Patch:
diff --git a/README.md b/README.md
index 27cd2ff..1c6f870 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # REST with Clean Architecture for Go
 
-[![Build Status](https://github.com/swaggest/rest/workflows/test/badge.svg)](https://github.com/swaggest/rest/actions?query=branch%3Amaster+workflow%3Atest)
+[![Build Status](https://github.com/swaggest/rest/workflows/test-unit/badge.svg)](https://github.com/swaggest/rest/actions?query=branch%3Amaster+workflow%3Atest-unit)
 [![Coverage Status](https://codecov.io/gh/swaggest/rest/branch/master/graph/badge.svg)](https://codecov.io/gh/swaggest/rest)
 [![GoDevDoc](https://img.shields.io/badge/dev-doc-00ADD8?logo=go)](https://pkg.go.dev/github.com/swaggest/rest)
 [![Time Tracker](https://wakatime.com/badge/github/swaggest/rest.svg)](https://wakatime.com/badge/github/swaggest/rest)
diff --git a/_examples/go.mod b/_examples/go.mod
index e6f6696..dbb4717 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -14,11 +14,11 @@ require (
 	github.com/rs/cors v1.8.2
 	github.com/stretchr/testify v1.8.1
 	github.com/swaggest/assertjson v1.7.0
-	github.com/swaggest/jsonschema-go v0.3.45
+	github.com/swaggest/jsonschema-go v0.3.47
 	github.com/swaggest/openapi-go v0.2.28
 	github.com/swaggest/swgui v1.5.1
 	github.com/swaggest/usecase v1.2.0
-	github.com/valyala/fasthttp v1.41.0
+	github.com/valyala/fasthttp v1.44.0
 )
 
 require (
@@ -33,7 +33,7 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/iancoleman/orderedmap v0.2.0 // indirect
-	github.com/klauspost/compress v1.15.12 // indirect
+	github.com/klauspost/compress v1.15.14 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
diff --git a/_examples/go.sum b/_examples/go.sum
index 3521c79..fc45e11 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -30,8 +30,8 @@ github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36
 github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
 github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
-github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
-github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
+github.com/klauspost/compress v1.15.14 h1:i7WCKDToww0wA+9qrUZ1xOjp218vfFo3nTU6UHp+gOc=
+github.com/klauspost/compress v1.15.14/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -62,8 +62,8 @@ github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ
 github.com/swaggest/assertjson v1.7.0/go.mod h1:vxMJMehbSVJd+dDWFCKv3QRZKNTpy/ktZKTz9LOEDng=
 github.com/swaggest/form/v5 v5.0.2 h1:TiimP7UX3q1nSI5ZGg6tGMmxg8UJB0JNpaZjXWH6V2E=
 github.com/swaggest/form/v5 v5.0.2/go.mod h1:Ayta1ggwSnDd4zwzv47jQL4RHk7WOTHp65nzwBc0IhU=
-github.com/swaggest/jsonschema-go v0.3.45 h1:h2RhU+K3HbI8c+giX4PWI8R96EW6/wtPHfagpAvdPfc=
-github.com/swaggest/jsonschema-go v0.3.45/go.mod h1:pW2jvwFFD2cQ0jv51bAo8+RLmma7yaUOIcH8s54EfJw=
+github.com/swaggest/jsonschema-go v0.3.47 h1:rS8BY1nvWZ5FMdcZnqPnh9Wkdrtx40SEXtRroG5IPe4=
+github.com/swaggest/jsonschema-go v0.3.47/go.mod h1:pW2jvwFFD2cQ0jv51bAo8+RLmma7yaUOIcH8s54EfJw=
 github.com/swaggest/openapi-go v0.2.28 h1:PRgJTqRfpsWte7qPqMFJooLu8cWyL91FhpYH2k1353Y=
 github.com/swaggest/openapi-go v0.2.28/go.mod h1:2RAaFLmRFQiR8TQJyIZzh2NTY1ihk3/WxYJFxQ+TfFU=
 github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
@@ -74,8 +74,8 @@ github.com/swaggest/usecase v1.2.0 h1:cHVFqxIbHfyTXp02JmWXk+ZADaSa87UZP+b3qL5Nz9
 github.com/swaggest/usecase v1.2.0/go.mod h1:oc5+QoAxG3Et5Gl9lRXgEOm00l4VN9gdVQSMIa5EeLY=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.41.0 h1:zeR0Z1my1wDHTRiamBCXVglQdbUwgb9uWG3k1HQz6jY=
-github.com/valyala/fasthttp v1.41.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
+github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q=
+github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
 github.com/vearutop/dynhist-go v1.1.0 h1:kufNShpt45I97L2eO3bEYTSPKBSued0prheukJUYRok=
 github.com/vearutop/dynhist-go v1.1.0/go.mod h1:mjWlwr64oGBtouxh8s/9YOXV73MYP0Q9tfKIAp8/xto=
diff --git a/go.mod b/go.mod
index a4eca5e..241fa22 100644
--- a/go.mod
+++ b/go.mod
@@ -12,7 +12,7 @@ require (
 	github.com/stretchr/testify v1.8.1
 	github.com/swaggest/assertjson v1.7.0
 	github.com/swaggest/form/v5 v5.0.2
-	github.com/swaggest/jsonschema-go v0.3.45
+	github.com/swaggest/jsonschema-go v0.3.47
 	github.com/swaggest/openapi-go v0.2.28
 	github.com/swaggest/refl v1.1.0
 	github.com/swaggest/usecase v1.2.0
diff --git a/go.sum b/go.sum
index c919314..0360bce 100644
--- a/go.sum
+++ b/go.sum
@@ -75,8 +75,9 @@ github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ
 github.com/swaggest/assertjson v1.7.0/go.mod h1:vxMJMehbSVJd+dDWFCKv3QRZKNTpy/ktZKTz9LOEDng=
 github.com/swaggest/form/v5 v5.0.2 h1:TiimP7UX3q1nSI5ZGg6tGMmxg8UJB0JNpaZjXWH6V2E=
 github.com/swaggest/form/v5 v5.0.2/go.mod h1:Ayta1ggwSnDd4zwzv47jQL4RHk7WOTHp65nzwBc0IhU=
-github.com/swaggest/jsonschema-go v0.3.45 h1:h2RhU+K3HbI8c+giX4PWI8R96EW6/wtPHfagpAvdPfc=
 github.com/swaggest/jsonschema-go v0.3.45/go.mod h1:pW2jvwFFD2cQ0jv51bAo8+RLmma7yaUOIcH8s54EfJw=
+github.com/swaggest/jsonschema-go v0.3.47 h1:rS8BY1nvWZ5FMdcZnqPnh9Wkdrtx40SEXtRroG5IPe4=
+github.com/swaggest/jsonschema-go v0.3.47/go.mod h1:pW2jvwFFD2cQ0jv51bAo8+RLmma7yaUOIcH8s54EfJw=
 github.com/swaggest/openapi-go v0.2.28 h1:PRgJTqRfpsWte7qPqMFJooLu8cWyL91FhpYH2k1353Y=
 github.com/swaggest/openapi-go v0.2.28/go.mod h1:2RAaFLmRFQiR8TQJyIZzh2NTY1ihk3/WxYJFxQ+TfFU=
 github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
diff --git a/jsonschema/validator.go b/jsonschema/validator.go
index e9a0d9b..3a3237a 100644
--- a/jsonschema/validator.go
+++ b/jsonschema/validator.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"net/http"
 
 	"github.com/santhosh-tekuri/jsonschema/v3"
 	"github.com/swaggest/rest"
@@ -113,6 +114,10 @@ func (v *Validator) AddSchema(in rest.ParamIn, name string, jsonSchema []byte, r
 	}
 
 	if required {
+		if in == rest.ParamInHeader {
+			name = http.CanonicalHeaderKey(name)
+		}
+
 		v.inRequired[in] = append(v.inRequired[in], name)
 	}
 


Test patch:
diff --git a/request/decoder_test.go b/request/decoder_test.go
index 9070d6e..e2e11c1 100644
--- a/request/decoder_test.go
+++ b/request/decoder_test.go
@@ -174,7 +174,21 @@ func TestDecoder_Decode_required(t *testing.T) {
 		MakeRequestValidator(http.MethodPost, input, nil)
 
 	err = dec.Decode(req, input, validator)
-	assert.Equal(t, rest.ValidationErrors{"header:X-In-HeAdEr": []string{"missing value"}}, err)
+	assert.Equal(t, rest.ValidationErrors{"header:X-In-Header": []string{"missing value"}}, err)
+}
+
+func TestDecoder_Decode_required_header_case(t *testing.T) {
+	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/", nil)
+	req.Header.Set("x-In-heAdEr", "123")
+	assert.NoError(t, err)
+
+	input := new(reqTest)
+	dec := request.NewDecoderFactory().MakeDecoder(http.MethodPost, input, nil)
+	validator := jsonschema.NewFactory(&openapi.Collector{}, &openapi.Collector{}).
+		MakeRequestValidator(http.MethodPost, input, nil)
+
+	err = dec.Decode(req, input, validator)
+	assert.NoError(t, err)
 }
 
 func TestDecoder_Decode_json(t *testing.T) {
diff --git a/request/factory_test.go b/request/factory_test.go
index d7e05db..3f3cdfd 100644
--- a/request/factory_test.go
+++ b/request/factory_test.go
@@ -2,6 +2,7 @@ package request_test
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"net/url"
 	"strings"
@@ -208,3 +209,33 @@ func TestDecoderFactory_MakeDecoder_customMapping(t *testing.T) {
 	assert.Equal(t, "bar", i.Name)
 	assert.Equal(t, 321, i.ID)
 }
+
+func TestDecoderFactory_MakeDecoder_header_case_sensitivity(t *testing.T) {
+	df := request.NewDecoderFactory()
+
+	type input struct {
+		A string `header:"x-one-two-three" required:"true"`
+		B string `header:"X-One-Two-Three"`
+		C string `header:"X-One-two-three"`
+		D string `header:"x-one-two-three"`
+	}
+
+	d := df.MakeDecoder(http.MethodGet, input{}, nil)
+
+	var v input
+
+	req, err := http.NewRequest(http.MethodGet, "/", nil)
+	require.NoError(t, err)
+
+	req.Header.Set("x-One-Two-threE", "hello!")
+
+	require.NoError(t, d.Decode(req, &v, rest.ValidatorFunc(func(in rest.ParamIn, namedData map[string]interface{}) error {
+		fmt.Printf("%+v", namedData)
+
+		return nil
+	})))
+	assert.Equal(t, "hello!", v.A)
+	assert.Equal(t, "hello!", v.B)
+	assert.Equal(t, "hello!", v.C)
+	assert.Equal(t, "hello!", v.D)
+}