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 08a4387b7 fix(protocol/triple): SendHeader writes into ResponseHeader 
instead of RequestHeader (#3668)
08a4387b7 is described below

commit 08a4387b7ea295b8068de0bc5cd64c3df92c9d48
Author: Lcos <[email protected]>
AuthorDate: Mon Aug 17 17:31:24 2026 +0800

    fix(protocol/triple): SendHeader writes into ResponseHeader instead of 
RequestHeader (#3668)
    
    SendHeader merged user headers into conn.RequestHeader(), so the headers
    were never flushed to the client (grpcHandlerConn.Send only flushes
    hc.responseHeader) and the inbound request header map was polluted.
    Align with the sibling SetHeader by writing into conn.ResponseHeader().
    
    Tests:
    - header_test.go: upgrade mockHandlerConn to expose a real RequestHeader
      and a Send call counter; add TestSendHeader and 
TestSendHeaderOutsideHandler.
    - triple_ext_test.go: add TestSendHeaderInUnaryHandler, an end-to-end test
      over a real HTTP transport that exercises net/http's WriteHeader snapshot
      semantics (the mock does not) and the unary handler's double-Send path
      (SendHeader's conn.Send(nil) followed by the framework's conn.Send(msg)).
    
    For #3667.
    
    Signed-off-by: Lcos-000 <[email protected]>
---
 protocol/triple/triple_protocol/header.go          |  2 +-
 protocol/triple/triple_protocol/header_test.go     | 38 +++++++++++++++++++---
 protocol/triple/triple_protocol/triple_ext_test.go | 38 ++++++++++++++++++++++
 3 files changed, 73 insertions(+), 5 deletions(-)

diff --git a/protocol/triple/triple_protocol/header.go 
b/protocol/triple/triple_protocol/header.go
index f367364e7..bc8c2715a 100644
--- a/protocol/triple/triple_protocol/header.go
+++ b/protocol/triple/triple_protocol/header.go
@@ -309,6 +309,6 @@ func SendHeader(ctx context.Context, header http.Header) 
error {
        if !ok {
                return errorf(CodeInternal, "triple: handler outgoing context 
not found; SendHeader must be called within a Triple handler")
        }
-       mergeHeaders(conn.RequestHeader(), header)
+       mergeHeaders(conn.ResponseHeader(), header)
        return conn.Send(nil)
 }
diff --git a/protocol/triple/triple_protocol/header_test.go 
b/protocol/triple/triple_protocol/header_test.go
index c6653d8a1..914e846bd 100644
--- a/protocol/triple/triple_protocol/header_test.go
+++ b/protocol/triple/triple_protocol/header_test.go
@@ -165,26 +165,29 @@ func TestAppendToOutgoingContextPanicsOnOddKV(t 
*testing.T) {
        })
 }
 
-// mockHandlerConn is a minimal StreamingHandlerConn for testing SetHeader and
-// SetTrailer.
+// mockHandlerConn is a minimal StreamingHandlerConn for testing SetHeader,
+// SendHeader and SetTrailer.
 type mockHandlerConn struct {
        responseHdr  http.Header
        responseTrlr http.Header
+       requestHdr   http.Header
+       sendCalls    int
 }
 
 func newMockHandlerConn() *mockHandlerConn {
        return &mockHandlerConn{
                responseHdr:  make(http.Header),
                responseTrlr: make(http.Header),
+               requestHdr:   make(http.Header),
        }
 }
 
 func (m *mockHandlerConn) Spec() Spec                    { return Spec{} }
 func (m *mockHandlerConn) Peer() Peer                    { return Peer{} }
 func (m *mockHandlerConn) Receive(any) error             { return nil }
-func (m *mockHandlerConn) RequestHeader() http.Header    { return nil }
+func (m *mockHandlerConn) RequestHeader() http.Header    { return m.requestHdr 
}
 func (m *mockHandlerConn) ExportableHeader() http.Header { return nil }
-func (m *mockHandlerConn) Send(any) error                { return nil }
+func (m *mockHandlerConn) Send(any) error                { m.sendCalls++; 
return nil }
 func (m *mockHandlerConn) ResponseHeader() http.Header   { return 
m.responseHdr }
 func (m *mockHandlerConn) ResponseTrailer() http.Header  { return 
m.responseTrlr }
 
@@ -210,6 +213,33 @@ func TestSetTrailer(t *testing.T) {
        assert.Equal(t, conn.responseTrlr.Get("X-Trailer"), "end")
 }
 
+// TestSendHeader verifies that SendHeader merges headers into the response
+// header buffer (not the request header) and triggers a flush via Send.
+func TestSendHeader(t *testing.T) {
+       t.Parallel()
+       conn := newMockHandlerConn()
+       ctx := context.WithValue(context.Background(), handlerOutgoingKey{}, 
conn)
+       err := SendHeader(ctx, http.Header{"X-Custom": []string{"value"}})
+       assert.Nil(t, err)
+       // Headers must land in the response header buffer.
+       assert.Equal(t, conn.responseHdr.Get("X-Custom"), "value")
+       // The request header must not be polluted.
+       assert.Equal(t, len(conn.requestHdr), 0)
+       // Send must be called exactly once to flush the headers.
+       assert.Equal(t, conn.sendCalls, 1)
+}
+
+// TestSendHeaderOutsideHandler verifies that SendHeader returns a CodeInternal
+// error when called outside a Triple handler context.
+func TestSendHeaderOutsideHandler(t *testing.T) {
+       t.Parallel()
+       err := SendHeader(context.Background(), http.Header{"X-Custom": 
[]string{"value"}})
+       assert.NotNil(t, err)
+       tripleErr, ok := asError(err)
+       assert.True(t, ok)
+       assert.Equal(t, tripleErr.Code(), CodeInternal)
+}
+
 // TestSetHeaderOutsideHandler verifies that SetHeader returns a CodeInternal
 // error when called outside a Triple handler context.
 func TestSetHeaderOutsideHandler(t *testing.T) {
diff --git a/protocol/triple/triple_protocol/triple_ext_test.go 
b/protocol/triple/triple_protocol/triple_ext_test.go
index 53a3356c0..af01c6fd8 100644
--- a/protocol/triple/triple_protocol/triple_ext_test.go
+++ b/protocol/triple/triple_protocol/triple_ext_test.go
@@ -565,6 +565,44 @@ func TestSetHeaderAndSetTrailerInUnaryHandler(t 
*testing.T) {
        assert.Equal(t, response.Trailer().Values(handlerTrailer), 
[]string{trailerValue})
 }
 
+// TestSendHeaderInUnaryHandler verifies that SendHeader flushes response
+// headers over a real HTTP transport and that the response body stays
+// decodable. Unlike the mock in header_test.go, this exercises net/http's
+// WriteHeader snapshot semantics and the unary handler's double-Send path
+// (SendHeader's conn.Send(nil) followed by the framework's conn.Send(msg)),
+// guarding the regression fixed in #3667.
+func TestSendHeaderInUnaryHandler(t *testing.T) {
+       t.Parallel()
+
+       handler := triple.NewUnaryHandler(
+               "/connect.ping.v1.PingService/Ping",
+               func() any { return new(pingv1.PingRequest) },
+               func(ctx context.Context, req *triple.Request) 
(*triple.Response, error) {
+                       if err := triple.SendHeader(ctx, 
http.Header{handlerHeader: []string{headerValue}}); err != nil {
+                               return nil, err
+                       }
+                       msg := req.Msg.(*pingv1.PingRequest)
+                       return triple.NewResponse(&pingv1.PingResponse{
+                               Number: msg.Number,
+                               Text:   msg.Text,
+                       }), nil
+               },
+       )
+       server := httptest.NewServer(handler)
+       t.Cleanup(server.Close)
+
+       client := pingv1connect.NewPingServiceClient(server.Client(), 
server.URL)
+       request := triple.NewRequest(&pingv1.PingRequest{Number: 42, Text: 
"hello"})
+       response := triple.NewResponse(&pingv1.PingResponse{})
+       err := client.Ping(context.Background(), request, response)
+       assert.Nil(t, err)
+       // The flushed response header must reach the client.
+       assert.Equal(t, response.Header().Values(handlerHeader), 
[]string{headerValue})
+       // The response body must remain decodable after the header flush.
+       assert.Equal(t, response.Msg.(*pingv1.PingResponse).Number, int64(42))
+       assert.Equal(t, response.Msg.(*pingv1.PingResponse).Text, "hello")
+}
+
 func TestConcurrentStreams(t *testing.T) {
        if testing.Short() {
                t.Skipf("skipping %s test in short mode", t.Name())

Reply via email to