From e614562733cb599c97f89d0bcf0a9444a5d7c09a Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Wed, 1 Feb 2023 19:11:54 +0800 Subject: [PATCH 1/8] metadata: fix validation issues --- internal/metadata/metadata.go | 3 ++ internal/metadata/metadata_test.go | 4 +++ stream.go | 20 ++++++++++-- test/metadata_test.go | 50 +++++++++++++++++++++++------- 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index b2980f8ac44a..f97937be87f2 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -84,6 +84,9 @@ func Set(addr resolver.Address, md metadata.MD) resolver.Address { // - otherwise, the header value must contain one or more characters from the set [%x20-%x7E]. func Validate(md metadata.MD) error { for k, vals := range md { + if k == "" { + return fmt.Errorf("there is an empty key in the header") + } // pseudo-header will be ignored if k[0] == ':' { continue diff --git a/internal/metadata/metadata_test.go b/internal/metadata/metadata_test.go index 80f1a44bb6ac..a06443f00828 100644 --- a/internal/metadata/metadata_test.go +++ b/internal/metadata/metadata_test.go @@ -100,6 +100,10 @@ func TestValidate(t *testing.T) { md: map[string][]string{"test": {string(rune(0x19))}}, want: errors.New("header key \"test\" contains value with non-printable ASCII characters"), }, + { + md: map[string][]string{"": {string(rune(0x19))}}, + want: errors.New("there is an empty key in the header"), + }, { md: map[string][]string{"test-bin": {string(rune(0x19))}}, want: nil, diff --git a/stream.go b/stream.go index 89936a4f1665..e4aa368d4279 100644 --- a/stream.go +++ b/stream.go @@ -168,8 +168,24 @@ func NewClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth } func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) { - if md, _, ok := metadata.FromOutgoingContextRaw(ctx); ok { - if err := imetadata.Validate(md); err != nil { + // validate original input + if md, added, ok := metadata.FromOutgoingContextRaw(ctx); ok { + // copy the original MD to avoid changing it + mdSize := len(md) + for i := range added { + mdSize += len(added[i]) / 2 + } + merged := make(metadata.MD, mdSize) + for k, v := range md { + merged[k] = v + } + for _, pairs := range added { + for i := 0; i < len(pairs); i += 2 { + k := pairs[i] + merged[k] = append(merged[k], pairs[i+1]) + } + } + if err := imetadata.Validate(merged); err != nil { return nil, status.Error(codes.Internal, err.Error()) } } diff --git a/test/metadata_test.go b/test/metadata_test.go index ad2b12cfc77f..dfd70945bc7f 100644 --- a/test/metadata_test.go +++ b/test/metadata_test.go @@ -39,9 +39,10 @@ func (s) TestInvalidMetadata(t *testing.T) { grpctest.TLogger.ExpectErrorN("stream: failed to validate md when setting trailer", 2) tests := []struct { - md metadata.MD - want error - recv error + md metadata.MD + appendMD []string + want error + recv error }{ { md: map[string][]string{string(rune(0x19)): {"testVal"}}, @@ -53,6 +54,23 @@ func (s) TestInvalidMetadata(t *testing.T) { want: status.Error(codes.Internal, "header key \"test\" contains value with non-printable ASCII characters"), recv: status.Error(codes.Internal, "invalid header field"), }, + { + md: map[string][]string{"test": {"test"}}, + appendMD: []string{"/", "value"}, + want: status.Error(codes.Internal, "header key \"/\" contains illegal characters not in [0-9a-z-_.]"), + recv: status.Error(codes.Internal, "invalid header field"), + }, + { + md: map[string][]string{"test": {"test"}}, + appendMD: []string{"", "value"}, + want: status.Error(codes.Internal, "there is an empty key in the header"), + recv: status.Error(codes.Internal, "invalid header field"), + }, + { + md: map[string][]string{"": {"test"}}, + want: status.Error(codes.Internal, "there is an empty key in the header"), + recv: status.Error(codes.Internal, "invalid header field"), + }, { md: map[string][]string{"test-bin": {string(rune(0x19))}}, want: nil, @@ -77,13 +95,22 @@ func (s) TestInvalidMetadata(t *testing.T) { } test := tests[testNum] testNum++ - if err := stream.SetHeader(test.md); !reflect.DeepEqual(test.want, err) { - return fmt.Errorf("call stream.SendHeader(md) validate metadata which is %v got err :%v, want err :%v", test.md, err, test.want) + // merge md + md := make(metadata.MD) + for k, v := range test.md { + md[k] = v + } + for i := 0; i < len(test.appendMD); i += 2 { + md[test.appendMD[i]] = append(md[test.appendMD[i]], test.appendMD[i+1]) } - if err := stream.SendHeader(test.md); !reflect.DeepEqual(test.want, err) { - return fmt.Errorf("call stream.SendHeader(md) validate metadata which is %v got err :%v, want err :%v", test.md, err, test.want) + + if err := stream.SetHeader(md); !reflect.DeepEqual(test.want, err) { + return fmt.Errorf("call stream.SendHeader(md) validate metadata which is %v got err :%v, want err :%v", md, err, test.want) } - stream.SetTrailer(test.md) + if err := stream.SendHeader(md); !reflect.DeepEqual(test.want, err) { + return fmt.Errorf("call stream.SendHeader(md) validate metadata which is %v got err :%v, want err :%v", md, err, test.want) + } + stream.SetTrailer(md) return nil }, } @@ -94,21 +121,21 @@ func (s) TestInvalidMetadata(t *testing.T) { for _, test := range tests { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - ctx = metadata.NewOutgoingContext(ctx, test.md) + ctx = metadata.AppendToOutgoingContext(ctx, test.appendMD...) if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); !reflect.DeepEqual(test.want, err) { t.Errorf("call ss.Client.EmptyCall() validate metadata which is %v got err :%v, want err :%v", test.md, err, test.want) } + cancel() } // call the stream server's api to drive the server-side unit testing for _, test := range tests { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) stream, err := ss.Client.FullDuplexCall(ctx) - defer cancel() if err != nil { t.Errorf("call ss.Client.FullDuplexCall(context.Background()) will success but got err :%v", err) + cancel() continue } if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil { @@ -117,5 +144,6 @@ func (s) TestInvalidMetadata(t *testing.T) { if _, err := stream.Recv(); status.Code(err) != status.Code(test.recv) || !strings.Contains(err.Error(), test.recv.Error()) { t.Errorf("stream.Recv() = _, get err :%v, want err :%v", err, test.recv) } + cancel() } } From f514755225badd59d1b5a54ea4bcbde15d7725ee Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Wed, 1 Feb 2023 20:12:11 +0800 Subject: [PATCH 2/8] fix logger test --- test/metadata_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/metadata_test.go b/test/metadata_test.go index dfd70945bc7f..d25f792c64d4 100644 --- a/test/metadata_test.go +++ b/test/metadata_test.go @@ -36,7 +36,7 @@ import ( ) func (s) TestInvalidMetadata(t *testing.T) { - grpctest.TLogger.ExpectErrorN("stream: failed to validate md when setting trailer", 2) + grpctest.TLogger.ExpectErrorN("stream: failed to validate md when setting trailer", 5) tests := []struct { md metadata.MD From 8bc5ce1614cbdc0258b5cb74984692714ffab807 Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Fri, 3 Feb 2023 16:51:39 +0800 Subject: [PATCH 3/8] Use a separate function to validate the appended pair to avoid copying the original md --- internal/metadata/metadata.go | 57 ++++++++++++++++++++--------------- stream.go | 29 ++++++++---------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index f97937be87f2..9d93ef8e1a71 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -82,33 +82,14 @@ func Set(addr resolver.Address, md metadata.MD) resolver.Address { // - header names must contain one or more characters from this set [0-9 a-z _ - .]. // - if the header-name ends with a "-bin" suffix, no validation of the header value is performed. // - otherwise, the header value must contain one or more characters from the set [%x20-%x7E]. -func Validate(md metadata.MD) error { +func Validate(md metadata.MD) (err error) { for k, vals := range md { - if k == "" { - return fmt.Errorf("there is an empty key in the header") - } - // pseudo-header will be ignored - if k[0] == ':' { - continue - } - // check key, for i that saving a conversion if not using for range - for i := 0; i < len(k); i++ { - r := k[i] - if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '.' && r != '-' && r != '_' { - return fmt.Errorf("header key %q contains illegal characters not in [0-9a-z-_.]", k) - } - } - if strings.HasSuffix(k, "-bin") { - continue - } - // check value - for _, val := range vals { - if hasNotPrintable(val) { - return fmt.Errorf("header key %q contains value with non-printable ASCII characters", k) - } + err = ValidatePair(k, vals...) + if err != nil { + break } } - return nil + return } // hasNotPrintable return true if msg contains any characters which are not in %x20-%x7E @@ -121,3 +102,31 @@ func hasNotPrintable(msg string) bool { } return false } + +// ValidatePair validate single pair in metadata +func ValidatePair(key string, vals ...string) error { + if key == "" { + return fmt.Errorf("there is an empty key in the header") + } + // pseudo-header will be ignored + if key[0] == ':' { + return nil + } + // check key, for i that saving a conversion if not using for range + for i := 0; i < len(key); i++ { + r := key[i] + if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '.' && r != '-' && r != '_' { + return fmt.Errorf("header key %q contains illegal characters not in [0-9a-z-_.]", key) + } + } + if strings.HasSuffix(key, "-bin") { + return nil + } + // check value + for _, val := range vals { + if hasNotPrintable(val) { + return fmt.Errorf("header key %q contains value with non-printable ASCII characters", key) + } + } + return nil +} diff --git a/stream.go b/stream.go index e4aa368d4279..ae3e62abcbf7 100644 --- a/stream.go +++ b/stream.go @@ -168,26 +168,21 @@ func NewClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth } func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) { - // validate original input if md, added, ok := metadata.FromOutgoingContextRaw(ctx); ok { - // copy the original MD to avoid changing it - mdSize := len(md) - for i := range added { - mdSize += len(added[i]) / 2 - } - merged := make(metadata.MD, mdSize) - for k, v := range md { - merged[k] = v - } - for _, pairs := range added { - for i := 0; i < len(pairs); i += 2 { - k := pairs[i] - merged[k] = append(merged[k], pairs[i+1]) - } - } - if err := imetadata.Validate(merged); err != nil { + // validate md + err := imetadata.Validate(md) + if err != nil { return nil, status.Error(codes.Internal, err.Error()) } + // validate added + for _, kvs := range added { + for i := 0; i < len(kvs); i += 2 { + err = imetadata.ValidatePair(kvs[i], kvs[i+1]) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + } + } } if channelz.IsOn() { cc.incrCallsStarted() From e21027b3736734b2deb33f1ade1e5fac43640a51 Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Fri, 3 Feb 2023 16:51:48 +0800 Subject: [PATCH 4/8] improve the test --- test/metadata_test.go | 62 ++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/test/metadata_test.go b/test/metadata_test.go index d25f792c64d4..2581b44d86f5 100644 --- a/test/metadata_test.go +++ b/test/metadata_test.go @@ -39,44 +39,52 @@ func (s) TestInvalidMetadata(t *testing.T) { grpctest.TLogger.ExpectErrorN("stream: failed to validate md when setting trailer", 5) tests := []struct { + name string md metadata.MD appendMD []string want error recv error }{ { + name: "invalid key", md: map[string][]string{string(rune(0x19)): {"testVal"}}, want: status.Error(codes.Internal, "header key \"\\x19\" contains illegal characters not in [0-9a-z-_.]"), recv: status.Error(codes.Internal, "invalid header field"), }, { + name: "invalid value", md: map[string][]string{"test": {string(rune(0x19))}}, want: status.Error(codes.Internal, "header key \"test\" contains value with non-printable ASCII characters"), recv: status.Error(codes.Internal, "invalid header field"), }, { + name: "invalid appended value", md: map[string][]string{"test": {"test"}}, appendMD: []string{"/", "value"}, want: status.Error(codes.Internal, "header key \"/\" contains illegal characters not in [0-9a-z-_.]"), recv: status.Error(codes.Internal, "invalid header field"), }, { + name: "empty appended key", md: map[string][]string{"test": {"test"}}, appendMD: []string{"", "value"}, want: status.Error(codes.Internal, "there is an empty key in the header"), recv: status.Error(codes.Internal, "invalid header field"), }, { + name: "empty key", md: map[string][]string{"": {"test"}}, want: status.Error(codes.Internal, "there is an empty key in the header"), recv: status.Error(codes.Internal, "invalid header field"), }, { + name: "-bin key with arbitrary value", md: map[string][]string{"test-bin": {string(rune(0x19))}}, want: nil, recv: io.EOF, }, { + name: "valid key and value", md: map[string][]string{"test": {"value"}}, want: nil, recv: io.EOF, @@ -95,13 +103,10 @@ func (s) TestInvalidMetadata(t *testing.T) { } test := tests[testNum] testNum++ - // merge md - md := make(metadata.MD) - for k, v := range test.md { - md[k] = v - } + // merge original md and added md + md := test.md.Copy() for i := 0; i < len(test.appendMD); i += 2 { - md[test.appendMD[i]] = append(md[test.appendMD[i]], test.appendMD[i+1]) + md.Append(test.appendMD[i], test.appendMD[i+1]) } if err := stream.SetHeader(md); !reflect.DeepEqual(test.want, err) { @@ -120,30 +125,33 @@ func (s) TestInvalidMetadata(t *testing.T) { defer ss.Stop() for _, test := range tests { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - ctx = metadata.NewOutgoingContext(ctx, test.md) - ctx = metadata.AppendToOutgoingContext(ctx, test.appendMD...) - if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); !reflect.DeepEqual(test.want, err) { - t.Errorf("call ss.Client.EmptyCall() validate metadata which is %v got err :%v, want err :%v", test.md, err, test.want) - } - cancel() + t.Run("unary "+test.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ctx = metadata.NewOutgoingContext(ctx, test.md) + ctx = metadata.AppendToOutgoingContext(ctx, test.appendMD...) + if _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{}); !reflect.DeepEqual(test.want, err) { + t.Errorf("call ss.Client.EmptyCall() validate metadata which is %v got err :%v, want err :%v", test.md, err, test.want) + } + }) } // call the stream server's api to drive the server-side unit testing for _, test := range tests { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - stream, err := ss.Client.FullDuplexCall(ctx) - if err != nil { - t.Errorf("call ss.Client.FullDuplexCall(context.Background()) will success but got err :%v", err) - cancel() - continue - } - if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil { - t.Errorf("call ss.Client stream Send(nil) will success but got err :%v", err) - } - if _, err := stream.Recv(); status.Code(err) != status.Code(test.recv) || !strings.Contains(err.Error(), test.recv.Error()) { - t.Errorf("stream.Recv() = _, get err :%v, want err :%v", err, test.recv) - } - cancel() + t.Run("streaming "+test.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + stream, err := ss.Client.FullDuplexCall(ctx) + if err != nil { + t.Errorf("call ss.Client.FullDuplexCall(context.Background()) will success but got err :%v", err) + return + } + if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil { + t.Errorf("call ss.Client stream Send(nil) will success but got err :%v", err) + } + if _, err := stream.Recv(); status.Code(err) != status.Code(test.recv) || !strings.Contains(err.Error(), test.recv.Error()) { + t.Errorf("stream.Recv() = _, get err :%v, want err :%v", err, test.recv) + } + }) } } From fa750b72abe962aace5a9bd01ba524bb4a45e25e Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Fri, 10 Feb 2023 22:37:19 +0800 Subject: [PATCH 5/8] improve error handling --- internal/metadata/metadata.go | 9 ++++----- stream.go | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index 9d93ef8e1a71..c6ac7b4954fa 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -82,14 +82,13 @@ func Set(addr resolver.Address, md metadata.MD) resolver.Address { // - header names must contain one or more characters from this set [0-9 a-z _ - .]. // - if the header-name ends with a "-bin" suffix, no validation of the header value is performed. // - otherwise, the header value must contain one or more characters from the set [%x20-%x7E]. -func Validate(md metadata.MD) (err error) { +func Validate(md metadata.MD) error { for k, vals := range md { - err = ValidatePair(k, vals...) - if err != nil { - break + if err := ValidatePair(k, vals...); err != nil { + return err } } - return + return nil } // hasNotPrintable return true if msg contains any characters which are not in %x20-%x7E diff --git a/stream.go b/stream.go index ae3e62abcbf7..32cfa35f6857 100644 --- a/stream.go +++ b/stream.go @@ -170,15 +170,13 @@ func NewClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) { if md, added, ok := metadata.FromOutgoingContextRaw(ctx); ok { // validate md - err := imetadata.Validate(md) - if err != nil { + if err := imetadata.Validate(md); err != nil { return nil, status.Error(codes.Internal, err.Error()) } // validate added for _, kvs := range added { for i := 0; i < len(kvs); i += 2 { - err = imetadata.ValidatePair(kvs[i], kvs[i+1]) - if err != nil { + if err = imetadata.ValidatePair(kvs[i], kvs[i+1]); err != nil { return nil, status.Error(codes.Internal, err.Error()) } } From 282edb33ed176dbe9dfaecf9a858270910ef6549 Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Fri, 10 Feb 2023 22:37:39 +0800 Subject: [PATCH 6/8] improve the test --- internal/metadata/metadata_test.go | 2 +- test/metadata_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/metadata/metadata_test.go b/internal/metadata/metadata_test.go index a06443f00828..8f0e430e5ed4 100644 --- a/internal/metadata/metadata_test.go +++ b/internal/metadata/metadata_test.go @@ -101,7 +101,7 @@ func TestValidate(t *testing.T) { want: errors.New("header key \"test\" contains value with non-printable ASCII characters"), }, { - md: map[string][]string{"": {string(rune(0x19))}}, + md: map[string][]string{"": {"valid"}}, want: errors.New("there is an empty key in the header"), }, { diff --git a/test/metadata_test.go b/test/metadata_test.go index 2581b44d86f5..b7b4c7470543 100644 --- a/test/metadata_test.go +++ b/test/metadata_test.go @@ -103,8 +103,8 @@ func (s) TestInvalidMetadata(t *testing.T) { } test := tests[testNum] testNum++ - // merge original md and added md - md := test.md.Copy() + // merge original md and added md. + md := metadata.Join(test.md, metadata.Pairs(test.appendMD...)) for i := 0; i < len(test.appendMD); i += 2 { md.Append(test.appendMD[i], test.appendMD[i+1]) } From f69729441cd7543efc6753f140e68507d066b8d7 Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Sat, 11 Feb 2023 10:07:34 +0800 Subject: [PATCH 7/8] improve test and declaration of variable --- stream.go | 2 +- test/metadata_test.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/stream.go b/stream.go index 32cfa35f6857..34b0cb4593e9 100644 --- a/stream.go +++ b/stream.go @@ -176,7 +176,7 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth // validate added for _, kvs := range added { for i := 0; i < len(kvs); i += 2 { - if err = imetadata.ValidatePair(kvs[i], kvs[i+1]); err != nil { + if err := imetadata.ValidatePair(kvs[i], kvs[i+1]); err != nil { return nil, status.Error(codes.Internal, err.Error()) } } diff --git a/test/metadata_test.go b/test/metadata_test.go index b7b4c7470543..a15e5cb1c6e7 100644 --- a/test/metadata_test.go +++ b/test/metadata_test.go @@ -105,9 +105,6 @@ func (s) TestInvalidMetadata(t *testing.T) { testNum++ // merge original md and added md. md := metadata.Join(test.md, metadata.Pairs(test.appendMD...)) - for i := 0; i < len(test.appendMD); i += 2 { - md.Append(test.appendMD[i], test.appendMD[i+1]) - } if err := stream.SetHeader(md); !reflect.DeepEqual(test.want, err) { return fmt.Errorf("call stream.SendHeader(md) validate metadata which is %v got err :%v, want err :%v", md, err, test.want) From 083a379b5b2f2695d99e7899199f1571b425ae61 Mon Sep 17 00:00:00 2001 From: ktalg <416432526@qq.com> Date: Thu, 23 Feb 2023 14:38:07 +0800 Subject: [PATCH 8/8] improve comments --- internal/metadata/metadata.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index c6ac7b4954fa..c82e608e0773 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -76,12 +76,7 @@ func Set(addr resolver.Address, md metadata.MD) resolver.Address { return addr } -// Validate returns an error if the input md contains invalid keys or values. -// -// If the header is not a pseudo-header, the following items are checked: -// - header names must contain one or more characters from this set [0-9 a-z _ - .]. -// - if the header-name ends with a "-bin" suffix, no validation of the header value is performed. -// - otherwise, the header value must contain one or more characters from the set [%x20-%x7E]. +// Validate validates every pair in md with ValidatePair. func Validate(md metadata.MD) error { for k, vals := range md { if err := ValidatePair(k, vals...); err != nil { @@ -102,8 +97,14 @@ func hasNotPrintable(msg string) bool { return false } -// ValidatePair validate single pair in metadata +// ValidatePair validate a key-value pair with the following rules (the pseudo-header will be skipped) : +// +// - key must contain one or more characters. +// - the characters in the key must be contained in [0-9 a-z _ - .]. +// - if the key ends with a "-bin" suffix, no validation of the corresponding value is performed. +// - the characters in the every value must be printable (in [%x20-%x7E]). func ValidatePair(key string, vals ...string) error { + // key should not be empty if key == "" { return fmt.Errorf("there is an empty key in the header") }