This is an automated email from the ASF dual-hosted git repository.
Alanxtl pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new e741e0e7e fix(triple): canonicalize context metadata headers (#3428)
e741e0e7e is described below
commit e741e0e7e54ce1f7acda67e919eacb1e3519b440
Author: 吴杨帆 <[email protected]>
AuthorDate: Tue Jun 16 15:42:10 2026 +0800
fix(triple): canonicalize context metadata headers (#3428)
* fix(triple): canonicalize context metadata headers
Signed-off-by: wuyangfan <[email protected]>
* test(triple): cover reserved header normalization
* fix(triple): clone incoming context headers
* test(triple): avoid noncanonical header key lookup
* docs(triple): clarify metadata header casing
---------
Signed-off-by: wuyangfan <[email protected]>
---
protocol/invocation/rpcinvocation.go | 5 +--
protocol/invocation/rpcinvocation_test.go | 10 ++----
protocol/triple/triple_protocol/header.go | 13 +++++---
protocol/triple/triple_protocol/header_test.go | 37 ++++++++++++++++++++++
protocol/triple/triple_protocol/protocol_grpc.go | 2 ++
.../triple/triple_protocol/protocol_grpc_test.go | 10 ++++++
protocol/triple/triple_protocol/triple.go | 14 ++++++++
7 files changed, 77 insertions(+), 14 deletions(-)
diff --git a/protocol/invocation/rpcinvocation.go
b/protocol/invocation/rpcinvocation.go
index 8219c8c22..242489c4e 100644
--- a/protocol/invocation/rpcinvocation.go
+++ b/protocol/invocation/rpcinvocation.go
@@ -267,10 +267,11 @@ func (r *RPCInvocation) MergeAttachmentFromContext(ctx
context.Context) {
return
}
for k, v := range header {
+ key := strings.ToLower(k)
if len(v) == 1 {
- r.SetAttachment(k, v[0])
+ r.SetAttachment(key, v[0])
} else {
- r.SetAttachment(k, v)
+ r.SetAttachment(key, v)
}
}
}
diff --git a/protocol/invocation/rpcinvocation_test.go
b/protocol/invocation/rpcinvocation_test.go
index c20ac483d..2b1373d5b 100644
--- a/protocol/invocation/rpcinvocation_test.go
+++ b/protocol/invocation/rpcinvocation_test.go
@@ -371,13 +371,9 @@ func TestRPCInvocation_GetAttachmentAsContext(t
*testing.T) {
header := triple_protocol.ExtractFromOutgoingContext(ctx)
assert.NotNil(t, header)
- // Verify that string attachments are in the header
- // NewOutgoingContext stores keys as lowercase, so check both ways
- assert.Contains(t, header, "key1")
- assert.Equal(t, []string{"value1"}, header["key1"]) //nolint:staticcheck
-
- assert.Contains(t, header, "key2")
- assert.Equal(t, []string{"value2", "value3"}, header["key2"])
//nolint:staticcheck
+ // Verify that string attachments are in the header.
+ assert.Equal(t, []string{"value1"}, header.Values("key1"))
+ assert.Equal(t, []string{"value2", "value3"}, header.Values("key2"))
// key3 (int) should not be in the header since it's not a string
assert.NotContains(t, header, "key3")
diff --git a/protocol/triple/triple_protocol/header.go
b/protocol/triple/triple_protocol/header.go
index adc9f7ecb..79757716c 100644
--- a/protocol/triple/triple_protocol/header.go
+++ b/protocol/triple/triple_protocol/header.go
@@ -19,7 +19,6 @@ import (
"encoding/base64"
"fmt"
"net/http"
- "strings"
)
// EncodeBinaryHeader base64-encodes the data. It always emits unpadded values.
@@ -101,10 +100,13 @@ func newIncomingContext(ctx context.Context, data
http.Header) context.Context {
extraData, ok := ctx.Value(extraDataKey{}).(map[string]http.Header)
if !ok {
extraData = map[string]http.Header{}
+ } else {
+ extraData = cloneExtraData(extraData)
}
for key, vals := range data {
- header[strings.ToLower(key)] = vals
+ // Context headers use canonical keys so http.Header.Get/Values
work as expected.
+ header[http.CanonicalHeaderKey(key)] = append([]string(nil),
vals...)
}
extraData[headerIncomingKey] = header
@@ -128,7 +130,8 @@ func NewOutgoingContext(ctx context.Context, data
http.Header) context.Context {
var header = http.Header{}
for key, vals := range data {
- header[strings.ToLower(key)] = append([]string(nil), vals...)
+ // Context headers use canonical keys so http.Header.Get/Values
work as expected.
+ header[http.CanonicalHeaderKey(key)] = append([]string(nil),
vals...)
}
extraData, ok := ctx.Value(extraDataKey{}).(map[string]http.Header)
@@ -182,8 +185,8 @@ func AppendToOutgoingContext(ctx context.Context, kv
...string) context.Context
extraData[headerOutgoingKey] = header
}
for i := 0; i < len(kv); i += 2 {
- // todo(DMwangnima): think about lowering
- header.Add(strings.ToLower(kv[i]), kv[i+1])
+ key := http.CanonicalHeaderKey(kv[i])
+ header[key] = append(header[key], kv[i+1])
}
return ctx
}
diff --git a/protocol/triple/triple_protocol/header_test.go
b/protocol/triple/triple_protocol/header_test.go
index 3b73b3570..febd96cc7 100644
--- a/protocol/triple/triple_protocol/header_test.go
+++ b/protocol/triple/triple_protocol/header_test.go
@@ -16,6 +16,8 @@ package triple_protocol
import (
"bytes"
+ "context"
+ "fmt"
"net/http"
"testing"
"testing/quick"
@@ -58,3 +60,38 @@ func TestHeaderMerge(t *testing.T) {
}
assert.Equal(t, header, expect)
}
+
+func TestNewIncomingContextClonesHeaders(t *testing.T) {
+ baseCtx := NewOutgoingContext(context.Background(), http.Header{
+ "Request-Id": []string{"outgoing"},
+ })
+ inputValues := []string{"incoming"}
+ input := http.Header{
+ "request-id": inputValues,
+ }
+
+ ctx := newIncomingContext(baseCtx, input)
+ incoming, ok := FromIncomingContext(ctx)
+ assert.True(t, ok)
+ incoming.Values("Request-Id")[0] = "changed"
+ incoming.Add("Another", "value")
+
+ assert.Equal(t, []string{"incoming"}, inputValues)
+ outgoing := ExtractFromOutgoingContext(baseCtx)
+ assert.Equal(t, []string{"outgoing"}, outgoing.Values("Request-Id"))
+}
+
+func ExampleNewOutgoingContext() {
+ ctx := NewOutgoingContext(context.Background(), http.Header{
+ "hello": []string{"triple"},
+ })
+ ctx = AppendToOutgoingContext(ctx, "hello", "dubbo", "hey", "hessian")
+
+ headers := ExtractFromOutgoingContext(ctx)
+ fmt.Println(headers.Values("hello"))
+ fmt.Println(headers.Get("hey"))
+
+ // Output:
+ // [triple dubbo]
+ // hessian
+}
diff --git a/protocol/triple/triple_protocol/protocol_grpc.go
b/protocol/triple/triple_protocol/protocol_grpc.go
index a663d3221..44f5bd4ef 100644
--- a/protocol/triple/triple_protocol/protocol_grpc.go
+++ b/protocol/triple/triple_protocol/protocol_grpc.go
@@ -476,6 +476,7 @@ func (hc *grpcHandlerConn) ExportableHeader() http.Header {
res := make(http.Header)
hdr := hc.request.Header
for key, vals := range hdr {
+ // Exported attachments stay lowercase to match Dubbo/gRPC
metadata keys.
key = strings.ToLower(key)
if IsReservedHeader(key) && !IsWhitelistedHeader(key) {
continue
@@ -492,6 +493,7 @@ func (hc *grpcHandlerConn) ExportableHeader() http.Header {
// reserved by gRPC protocol. Any other headers are classified as the
// user-specified metadata.
func IsReservedHeader(hdr string) bool {
+ hdr = strings.ToLower(hdr)
if hdr != "" && hdr[0] == ':' {
return true
}
diff --git a/protocol/triple/triple_protocol/protocol_grpc_test.go
b/protocol/triple/triple_protocol/protocol_grpc_test.go
index 6a454831a..f0aea1ac8 100644
--- a/protocol/triple/triple_protocol/protocol_grpc_test.go
+++ b/protocol/triple/triple_protocol/protocol_grpc_test.go
@@ -124,6 +124,16 @@ func TestGRPCHandlerSender(t *testing.T) {
})
}
+func TestIsReservedHeaderCanonicalizesInput(t *testing.T) {
+ t.Parallel()
+
+ for _, header := range []string{"Content-Type", "Grpc-Status", "TE"} {
+ if !IsReservedHeader(header) {
+ t.Fatalf("expected %q to be reserved", header)
+ }
+ }
+}
+
func testGRPCHandlerConnMetadata(t *testing.T, conn handlerConnCloser) {
// Closing the sender shouldn't unpredictably mutate user-visible
headers or
// trailers.
diff --git a/protocol/triple/triple_protocol/triple.go
b/protocol/triple/triple_protocol/triple.go
index 3ee8f0ce4..99b5bf642 100644
--- a/protocol/triple/triple_protocol/triple.go
+++ b/protocol/triple/triple_protocol/triple.go
@@ -230,6 +230,13 @@ func (r *Response) Any() any {
// Header returns the HTTP headers for this response. Headers beginning with
// "Triple-" and "Grpc-" are reserved for use by the Triple and gRPC
// protocols: applications may read them but shouldn't write them.
+//
+// Unary clients can pass a response wrapper to the generated client method,
+// then read response headers after the call returns:
+//
+// response := NewResponse(&greet.GreetResponse{})
+// err := client.Greet(ctx, NewRequest(&greet.GreetRequest{}), response)
+// values := response.Header().Values("hello")
func (r *Response) Header() http.Header {
if r.header == nil {
r.header = make(http.Header)
@@ -244,6 +251,13 @@ func (r *Response) Header() http.Header {
// Trailers beginning with "Triple-" and "Grpc-" are reserved for use by the
// Triple and gRPC protocols: applications may read them but shouldn't write
// them.
+//
+// Unary clients can read trailers from the same response wrapper after the
+// generated client method returns:
+//
+// response := NewResponse(&greet.GreetResponse{})
+// err := client.Greet(ctx, NewRequest(&greet.GreetRequest{}), response)
+// values := response.Trailer().Values("end")
func (r *Response) Trailer() http.Header {
if r.trailer == nil {
r.trailer = make(http.Header)