Repository: coreruleset/go-ftw
Pull request #331 (2024-07-09 11:29:21)

Header getter/setter should be case in-sensitive


Patch:
diff --git a/ftwhttp/header.go b/ftwhttp/header.go
index cb59332..abe80cb 100644
--- a/ftwhttp/header.go
+++ b/ftwhttp/header.go
@@ -6,6 +6,7 @@ package ftwhttp
 import (
 	"bytes"
 	"io"
+	"net/textproto"
 	"sort"
 
 	"github.com/rs/zerolog/log"
@@ -32,8 +33,8 @@ func (w stringWriter) WriteString(s string) (n int, err error) {
 	return w.w.Write([]byte(s))
 }
 
-// Add adds the key, value pair to the header.
-// It appends to any existing values associated with key.
+// Add adds the (key, value) pair to the headers if it does not exist
+// The key is case-insensitive
 func (h Header) Add(key, value string) {
 	if h.Get(key) == "" {
 		h.Set(key, value)
@@ -42,36 +43,33 @@ func (h Header) Add(key, value string) {
 
 // Set sets the header entries associated with key to
 // the single element value. It replaces any existing
-// values associated with key.
+// values associated with a case-insensitive key.
 func (h Header) Set(key, value string) {
+	h.Del(key)
 	h[key] = value
 }
 
-// Get gets the first value associated with the given key.
-// It is case insensitive;
+// Get gets the value associated with the given key.
 // If there are no values associated with the key, Get returns "".
+// The key is case-insensitive
 func (h Header) Get(key string) string {
 	if h == nil {
 		return ""
 	}
-	v := h[key]
+	v := h[h.getKeyMatchingCanonicalKey(key)]
 
 	return v
 }
 
-// Value returns the value associated with the given key.
-// It is case insensitive;
+// Value is a wrapper to Get
 func (h Header) Value(key string) string {
-	if h == nil {
-		return ""
-	}
-
-	return h[key]
+	return h.Get(key)
 }
 
 // Del deletes the value associated with key.
+// The key is case-insensitive
 func (h Header) Del(key string) {
-	delete(h, key)
+	delete(h, h.getKeyMatchingCanonicalKey(key))
 }
 
 // Write writes a header in wire format.
@@ -138,3 +136,20 @@ func (h Header) getSortedHeadersByName() []string {
 
 	return keys
 }
+
+// getKeyMatchingCanonicalKey finds a key matching with the given one, provided both are canonicalised
+func (h Header) getKeyMatchingCanonicalKey(searchKey string) string {
+	searchKey = canonicalKey(searchKey)
+	for k := range h {
+		if searchKey == canonicalKey(k) {
+			return k
+		}
+	}
+
+	return ""
+}
+
+// canonicalKey transforms given to the canonical form
+func canonicalKey(key string) string {
+	return textproto.CanonicalMIMEHeaderKey(key)
+}


Test patch:
diff --git a/ftwhttp/header_test.go b/ftwhttp/header_test.go
index 780c1d4..c911071 100644
--- a/ftwhttp/header_test.go
+++ b/ftwhttp/header_test.go
@@ -104,7 +104,7 @@ func (s *headerTestSuite) TestHeaderWriteString() {
 	}
 }
 
-func (s *headerTestSuite) TestHeaderSetGet() {
+func (s *headerTestSuite) TestHeaderAdd() {
 	h := Header{
 		"Custom": "Value",
 	}
@@ -113,6 +113,45 @@ func (s *headerTestSuite) TestHeaderSetGet() {
 	s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value")
 }
 
+func (s *headerTestSuite) TestHeaderAdd_CaseInsensitiveKey() {
+	h := Header{}
+
+	h.Add("camel-Header", "Value")
+	value := h.Get("Camel-Header")
+	s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value")
+
+	// Header exists, ignore overwriting
+	h.Add("Camel-Header", "Value2")
+	value = h.Get("Camel-Header")
+	s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value")
+
+	// Overwrite header
+	h.Set("Camel-Header", "Value3")
+	value = h.Get("Camel-Header")
+	s.Equalf("Value3", value, "got: %s, want: %s\n", value, "Value3")
+}
+
+func (s *headerTestSuite) TestHeaderGet() {
+	h := Header{
+		"Custom": "Value",
+	}
+	value := h.Get("Custom")
+	s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value")
+}
+
+func (s *headerTestSuite) TestHeaderGet_CaseInsensitiveKey() {
+	h := Header{
+		"Custom":        "Value",
+		"Custom-Header": "Value2",
+	}
+
+	value := h.Get("Custom")
+	s.Equalf("Value", value, "got: %s, want: %s\n", value, "Value")
+
+	value = h.Get("custom-header")
+	s.Equalf("Value2", value, "got: %s, want: %s\n", value, "Value2")
+}
+
 func (s *headerTestSuite) TestHeaderDel() {
 	for i, test := range headerWriteTests {
 		// we clone it because we are modifying the original
@@ -126,6 +165,14 @@ func (s *headerTestSuite) TestHeaderDel() {
 	}
 }
 
+func (s *headerTestSuite) TestHeaderDel_CaseInsensitiveKey() {
+	h := Header{}
+	h.Add("content-Type", "Value")
+	h.Del("Content-type")
+	value := h.Get("Content-Type")
+	s.Equalf("", value, "#case: got: %s, want: %s\n", value, "")
+}
+
 func (s *headerTestSuite) TestHeaderClone() {
 	h := Header{
 		"Custom": "Value",