Never encode binary metadata within the metadata map (#1188)
This change ensures consistency for the user when accessing metadata values:
they are never encoded except when sent on the wire. Previously, they would
appear encoded to client code, but not to server code. As such, this
represents a behavior change, but one unlikely to affect user code, as it's
unusual to inspect the metadata after setting it.
diff --git a/metadata/metadata.go b/metadata/metadata.go
index 7ca4418..5395ca8 100644
--- a/metadata/metadata.go
+++ b/metadata/metadata.go
@@ -36,46 +36,15 @@
package metadata // import "google.golang.org/grpc/metadata"
import (
- "encoding/base64"
"fmt"
"strings"
"golang.org/x/net/context"
)
-const (
- binHdrSuffix = "-bin"
-)
-
-// encodeKeyValue encodes key and value qualified for transmission via gRPC.
-// Transmitting binary headers violates HTTP/2 spec.
-// TODO(zhaoq): Maybe check if k is ASCII also.
-func encodeKeyValue(k, v string) (string, string) {
- k = strings.ToLower(k)
- if strings.HasSuffix(k, binHdrSuffix) {
- val := base64.StdEncoding.EncodeToString([]byte(v))
- v = string(val)
- }
- return k, v
-}
-
-// DecodeKeyValue returns the original key and value corresponding to the
-// encoded data in k, v.
-// If k is a binary header and v contains comma, v is split on comma before decoded,
-// and the decoded v will be joined with comma before returned.
+// DecodeKeyValue returns k, v, nil. It is deprecated and should not be used.
func DecodeKeyValue(k, v string) (string, string, error) {
- if !strings.HasSuffix(k, binHdrSuffix) {
- return k, v, nil
- }
- vvs := strings.Split(v, ",")
- for i, vv := range vvs {
- val, err := base64.StdEncoding.DecodeString(vv)
- if err != nil {
- return "", "", err
- }
- vvs[i] = string(val)
- }
- return k, strings.Join(vvs, ","), nil
+ return k, v, nil
}
// MD is a mapping from metadata keys to values. Users should use the following
@@ -83,11 +52,11 @@
type MD map[string][]string
// New creates a MD from given key-value map.
-// Keys are automatically converted to lowercase. And for keys having "-bin" as suffix, their values will be applied Base64 encoding.
+// Keys are automatically converted to lowercase.
func New(m map[string]string) MD {
md := MD{}
- for k, v := range m {
- key, val := encodeKeyValue(k, v)
+ for k, val := range m {
+ key := strings.ToLower(k)
md[key] = append(md[key], val)
}
return md
@@ -95,20 +64,19 @@
// Pairs returns an MD formed by the mapping of key, value ...
// Pairs panics if len(kv) is odd.
-// Keys are automatically converted to lowercase. And for keys having "-bin" as suffix, their values will be appplied Base64 encoding.
+// Keys are automatically converted to lowercase.
func Pairs(kv ...string) MD {
if len(kv)%2 == 1 {
panic(fmt.Sprintf("metadata: Pairs got the odd number of input pairs for metadata: %d", len(kv)))
}
md := MD{}
- var k string
+ var key string
for i, s := range kv {
if i%2 == 0 {
- k = s
+ key = strings.ToLower(s)
continue
}
- key, val := encodeKeyValue(k, s)
- md[key] = append(md[key], val)
+ md[key] = append(md[key], s)
}
return md
}
diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go
index fef0a0f..bd982cc 100644
--- a/metadata/metadata_test.go
+++ b/metadata/metadata_test.go
@@ -38,73 +38,20 @@
"testing"
)
-const binaryValue = string(128)
-
-func TestEncodeKeyValue(t *testing.T) {
- for _, test := range []struct {
- // input
- kin string
- vin string
- // output
- kout string
- vout string
- }{
- {"key", "abc", "key", "abc"},
- {"KEY", "abc", "key", "abc"},
- {"key-bin", "abc", "key-bin", "YWJj"},
- {"key-bin", binaryValue, "key-bin", "woA="},
- } {
- k, v := encodeKeyValue(test.kin, test.vin)
- if k != test.kout || !reflect.DeepEqual(v, test.vout) {
- t.Fatalf("encodeKeyValue(%q, %q) = %q, %q, want %q, %q", test.kin, test.vin, k, v, test.kout, test.vout)
- }
- }
-}
-
-func TestDecodeKeyValue(t *testing.T) {
- for _, test := range []struct {
- // input
- kin string
- vin string
- // output
- kout string
- vout string
- err error
- }{
- {"a", "abc", "a", "abc", nil},
- {"key-bin", "Zm9vAGJhcg==", "key-bin", "foo\x00bar", nil},
- {"key-bin", "woA=", "key-bin", binaryValue, nil},
- {"a", "abc,efg", "a", "abc,efg", nil},
- {"key-bin", "Zm9vAGJhcg==,Zm9vAGJhcg==", "key-bin", "foo\x00bar,foo\x00bar", nil},
- } {
- k, v, err := DecodeKeyValue(test.kin, test.vin)
- if k != test.kout || !reflect.DeepEqual(v, test.vout) || !reflect.DeepEqual(err, test.err) {
- t.Fatalf("DecodeKeyValue(%q, %q) = %q, %q, %v, want %q, %q, %v", test.kin, test.vin, k, v, err, test.kout, test.vout, test.err)
- }
- }
-}
-
func TestPairsMD(t *testing.T) {
for _, test := range []struct {
// input
kv []string
// output
- md MD
- size int
+ md MD
}{
- {[]string{}, MD{}, 0},
- {[]string{"k1", "v1", "k2-bin", binaryValue}, New(map[string]string{
- "k1": "v1",
- "k2-bin": binaryValue,
- }), 2},
+ {[]string{}, MD{}},
+ {[]string{"k1", "v1", "k1", "v2"}, MD{"k1": []string{"v1", "v2"}}},
} {
md := Pairs(test.kv...)
if !reflect.DeepEqual(md, test.md) {
t.Fatalf("Pairs(%v) = %v, want %v", test.kv, md, test.md)
}
- if md.Len() != test.size {
- t.Fatalf("Pairs(%v) generates md of size %d, want %d", test.kv, md.Len(), test.size)
- }
}
}
diff --git a/test/end2end_test.go b/test/end2end_test.go
index fd77cd7..6a36b68 100644
--- a/test/end2end_test.go
+++ b/test/end2end_test.go
@@ -75,8 +75,9 @@
var (
// For headers:
testMetadata = metadata.MD{
- "key1": []string{"value1"},
- "key2": []string{"value2"},
+ "key1": []string{"value1"},
+ "key2": []string{"value2"},
+ "key3-bin": []string{"binvalue1", string([]byte{1, 2, 3})},
}
testMetadata2 = metadata.MD{
"key1": []string{"value12"},
@@ -84,8 +85,9 @@
}
// For trailers:
testTrailerMetadata = metadata.MD{
- "tkey1": []string{"trailerValue1"},
- "tkey2": []string{"trailerValue2"},
+ "tkey1": []string{"trailerValue1"},
+ "tkey2": []string{"trailerValue2"},
+ "tkey3-bin": []string{"trailerbinvalue1", string([]byte{3, 2, 1})},
}
testTrailerMetadata2 = metadata.MD{
"tkey1": []string{"trailerValue12"},
diff --git a/transport/handler_server.go b/transport/handler_server.go
index 28c9ce0..24f306b 100644
--- a/transport/handler_server.go
+++ b/transport/handler_server.go
@@ -111,6 +111,10 @@
v = v[:i]
}
}
+ v, err := decodeMetadataHeader(k, v)
+ if err != nil {
+ return nil, streamErrorf(codes.InvalidArgument, "malformed binary metadata: %v", err)
+ }
metakv = append(metakv, k, v)
}
}
@@ -207,10 +211,9 @@
continue
}
for _, v := range vv {
- // http2 ResponseWriter mechanism to
- // send undeclared Trailers after the
- // headers have possibly been written.
- h.Add(http2.TrailerPrefix+k, v)
+ // http2 ResponseWriter mechanism to send undeclared Trailers after
+ // the headers have possibly been written.
+ h.Add(http2.TrailerPrefix+k, encodeMetadataHeader(k, v))
}
}
}
@@ -265,6 +268,7 @@
continue
}
for _, v := range vv {
+ v = encodeMetadataHeader(k, v)
h.Add(k, v)
}
}
diff --git a/transport/http2_client.go b/transport/http2_client.go
index 67a7945..380fff6 100644
--- a/transport/http2_client.go
+++ b/transport/http2_client.go
@@ -437,23 +437,23 @@
)
if md, ok := metadata.FromOutgoingContext(ctx); ok {
hasMD = true
- for k, v := range md {
+ for k, vv := range md {
// HTTP doesn't allow you to set pseudoheaders after non pseudoheaders were set.
if isReservedHeader(k) {
continue
}
- for _, entry := range v {
- t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: entry})
+ for _, v := range vv {
+ t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
}
}
}
if md, ok := t.md.(*metadata.MD); ok {
- for k, v := range *md {
+ for k, vv := range *md {
if isReservedHeader(k) {
continue
}
- for _, entry := range v {
- t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: entry})
+ for _, v := range vv {
+ t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
}
}
}
diff --git a/transport/http2_server.go b/transport/http2_server.go
index 31fefc7..14cd19c 100644
--- a/transport/http2_server.go
+++ b/transport/http2_server.go
@@ -644,13 +644,13 @@
if s.sendCompress != "" {
t.hEnc.WriteField(hpack.HeaderField{Name: "grpc-encoding", Value: s.sendCompress})
}
- for k, v := range md {
+ for k, vv := range md {
if isReservedHeader(k) {
// Clients don't tolerate reading restricted headers after some non restricted ones were sent.
continue
}
- for _, entry := range v {
- t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: entry})
+ for _, v := range vv {
+ t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
}
}
bufLen := t.hBuf.Len()
@@ -713,21 +713,17 @@
panic(err)
}
- for k, v := range metadata.New(map[string]string{"grpc-status-details-bin": (string)(stBytes)}) {
- for _, entry := range v {
- t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: entry})
- }
- }
+ t.hEnc.WriteField(hpack.HeaderField{Name: "grpc-status-details-bin", Value: encodeBinHeader(stBytes)})
}
// Attach the trailer metadata.
- for k, v := range s.trailer {
+ for k, vv := range s.trailer {
// Clients don't tolerate reading restricted headers after some non restricted ones were sent.
if isReservedHeader(k) {
continue
}
- for _, entry := range v {
- t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: entry})
+ for _, v := range vv {
+ t.hEnc.WriteField(hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
}
}
bufLen := t.hBuf.Len()
diff --git a/transport/http_util.go b/transport/http_util.go
index 89c1525..e5120eb 100644
--- a/transport/http_util.go
+++ b/transport/http_util.go
@@ -36,6 +36,7 @@
import (
"bufio"
"bytes"
+ "encoding/base64"
"fmt"
"io"
"net"
@@ -50,7 +51,6 @@
spb "google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
- "google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
@@ -164,6 +164,31 @@
return d.statusGen
}
+const binHdrSuffix = "-bin"
+
+func encodeBinHeader(v []byte) string {
+ return base64.StdEncoding.EncodeToString(v)
+}
+
+func decodeBinHeader(v string) ([]byte, error) {
+ return base64.StdEncoding.DecodeString(v)
+}
+
+func encodeMetadataHeader(k, v string) string {
+ if strings.HasSuffix(k, binHdrSuffix) {
+ return encodeBinHeader(([]byte)(v))
+ }
+ return v
+}
+
+func decodeMetadataHeader(k, v string) (string, error) {
+ if strings.HasSuffix(k, binHdrSuffix) {
+ b, err := decodeBinHeader(v)
+ return string(b), err
+ }
+ return v, nil
+}
+
func (d *decodeState) processHeaderField(f hpack.HeaderField) error {
switch f.Name {
case "content-type":
@@ -181,12 +206,12 @@
case "grpc-message":
d.rawStatusMsg = decodeGrpcMessage(f.Value)
case "grpc-status-details-bin":
- _, v, err := metadata.DecodeKeyValue("grpc-status-details-bin", f.Value)
+ v, err := decodeBinHeader(f.Value)
if err != nil {
return streamErrorf(codes.Internal, "transport: malformed grpc-status-details-bin: %v", err)
}
s := &spb.Status{}
- if err := proto.Unmarshal([]byte(v), s); err != nil {
+ if err := proto.Unmarshal(v, s); err != nil {
return streamErrorf(codes.Internal, "transport: malformed grpc-status-details-bin: %v", err)
}
d.statusGen = status.FromProto(s)
@@ -203,12 +228,12 @@
if d.mdata == nil {
d.mdata = make(map[string][]string)
}
- k, v, err := metadata.DecodeKeyValue(f.Name, f.Value)
+ v, err := decodeMetadataHeader(f.Name, f.Value)
if err != nil {
grpclog.Printf("Failed to decode (%q, %q): %v", f.Name, f.Value, err)
return nil
}
- d.mdata[k] = append(d.mdata[k], v)
+ d.mdata[f.Name] = append(d.mdata[f.Name], v)
}
}
return nil
diff --git a/transport/http_util_test.go b/transport/http_util_test.go
index a5f8a85..8e2c488 100644
--- a/transport/http_util_test.go
+++ b/transport/http_util_test.go
@@ -35,6 +35,7 @@
import (
"fmt"
+ "reflect"
"testing"
"time"
)
@@ -143,3 +144,46 @@
}
}
}
+
+const binaryValue = string(128)
+
+func TestEncodeMetadataHeader(t *testing.T) {
+ for _, test := range []struct {
+ // input
+ kin string
+ vin string
+ // output
+ vout string
+ }{
+ {"key", "abc", "abc"},
+ {"KEY", "abc", "abc"},
+ {"key-bin", "abc", "YWJj"},
+ {"key-bin", binaryValue, "woA="},
+ } {
+ v := encodeMetadataHeader(test.kin, test.vin)
+ if !reflect.DeepEqual(v, test.vout) {
+ t.Fatalf("encodeMetadataHeader(%q, %q) = %q, want %q", test.kin, test.vin, v, test.vout)
+ }
+ }
+}
+
+func TestDecodeMetadataHeader(t *testing.T) {
+ for _, test := range []struct {
+ // input
+ kin string
+ vin string
+ // output
+ vout string
+ err error
+ }{
+ {"a", "abc", "abc", nil},
+ {"key-bin", "Zm9vAGJhcg==", "foo\x00bar", nil},
+ {"key-bin", "woA=", binaryValue, nil},
+ {"a", "abc,efg", "abc,efg", nil},
+ } {
+ v, err := decodeMetadataHeader(test.kin, test.vin)
+ if !reflect.DeepEqual(v, test.vout) || !reflect.DeepEqual(err, test.err) {
+ t.Fatalf("decodeMetadataHeader(%q, %q) = %q, %v, want %q, %v", test.kin, test.vin, v, err, test.vout, test.err)
+ }
+ }
+}