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 9fcee8d69 feat(triple): expose unary response metadata via call
options (#3443)
9fcee8d69 is described below
commit 9fcee8d6907548cdec9614972d91b0f4bbf3c0bd
Author: ヴァニラシ <[email protected]>
AuthorDate: Fri Jun 19 16:03:18 2026 +0800
feat(triple): expose unary response metadata via call options (#3443)
* feat(triple): expose unary response metadata via call options
* fix(triple): capture unary response metadata on error
* docs(client): clarify response metadata capture on errors
* fix(triple): validate response metadata capture attributes
Signed-off-by: Vanillaxi <[email protected]>
* style(triple): format client test imports
---------
Signed-off-by: Vanillaxi <[email protected]>
---
client/client.go | 6 +++
client/client_test.go | 23 +++++++-
client/options.go | 25 ++++++++-
common/constant/key.go | 2 +
protocol/triple/client.go | 12 +++--
protocol/triple/client_test.go | 89 +++++++++++++++++++++++++++++++
protocol/triple/triple_invoker.go | 28 +++++++++-
protocol/triple/triple_protocol/triple.go | 6 ++-
8 files changed, 180 insertions(+), 11 deletions(-)
diff --git a/client/client.go b/client/client.go
index 794da976d..c15514830 100644
--- a/client/client.go
+++ b/client/client.go
@@ -277,6 +277,12 @@ func generateInvocation(ctx context.Context, methodName
string, reqs []any, resp
invocation.WithAttachments(attachments),
)
inv.SetAttribute(constant.CallTypeKey, callType)
+ if opts.ResponseHeader != nil {
+ inv.SetAttribute(constant.ResponseHeaderKey,
opts.ResponseHeader)
+ }
+ if opts.ResponseTrailer != nil {
+ inv.SetAttribute(constant.ResponseTrailerKey,
opts.ResponseTrailer)
+ }
return inv, nil
}
diff --git a/client/client_test.go b/client/client_test.go
index 95d2e5ea1..837925a67 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -20,6 +20,7 @@ package client
import (
"context"
"errors"
+ "net/http"
"testing"
"time"
)
@@ -146,7 +147,19 @@ func TestConnectionCallPassesOptions(t *testing.T) {
conn := &Connection{refOpts: &ReferenceOptions{invoker: invoker}}
var resp string
- res, err := conn.call(context.Background(), []any{"req"}, &resp,
"Ping", constant.CallUnary, WithCallRequestTimeout(1500*time.Millisecond),
WithCallRetries(3))
+ var responseHeader http.Header
+ var responseTrailer http.Header
+ res, err := conn.call(
+ context.Background(),
+ []any{"req"},
+ &resp,
+ "Ping",
+ constant.CallUnary,
+ WithCallRequestTimeout(1500*time.Millisecond),
+ WithCallRetries(3),
+ WithResponseHeader(&responseHeader),
+ WithResponseTrailer(&responseTrailer),
+ )
require.NoError(t, err)
require.Equal(t, invRes, res)
@@ -158,6 +171,14 @@ func TestConnectionCallPassesOptions(t *testing.T) {
requireCallType(t, inv, constant.CallUnary)
require.Equal(t, []any{"req", &resp}, inv.ParameterRawValues())
+
+ headerTarget, ok := inv.GetAttribute(constant.ResponseHeaderKey)
+ require.True(t, ok)
+ require.Same(t, &responseHeader, headerTarget)
+
+ trailerTarget, ok := inv.GetAttribute(constant.ResponseTrailerKey)
+ require.True(t, ok)
+ require.Same(t, &responseTrailer, trailerTarget)
}
func TestCallUnary(t *testing.T) {
diff --git a/client/options.go b/client/options.go
index 2be018441..cae30d2e1 100644
--- a/client/options.go
+++ b/client/options.go
@@ -18,6 +18,7 @@
package client
import (
+ "net/http"
"strconv"
"time"
)
@@ -999,8 +1000,10 @@ func SetClientRouters(routers []*global.RouterConfig)
ClientOption {
// todo: need to be consistent with MethodConfig
type CallOptions struct {
- RequestTimeout string
- Retries string
+ RequestTimeout string
+ Retries string
+ ResponseHeader *http.Header
+ ResponseTrailer *http.Header
}
type CallOption func(*CallOptions)
@@ -1022,3 +1025,21 @@ func WithCallRetries(retries int) CallOption {
opts.Retries = strconv.Itoa(retries)
}
}
+
+// WithResponseHeader configures a target to receive response headers.
+// Currently, only Triple unary calls populate this option (including error
+// responses when metadata is available).
+func WithResponseHeader(header *http.Header) CallOption {
+ return func(opts *CallOptions) {
+ opts.ResponseHeader = header
+ }
+}
+
+// WithResponseTrailer configures a target to receive response trailers.
+// Currently, only Triple unary calls populate this option (including error
+// responses when metadata is available).
+func WithResponseTrailer(trailer *http.Header) CallOption {
+ return func(opts *CallOptions) {
+ opts.ResponseTrailer = trailer
+ }
+}
diff --git a/common/constant/key.go b/common/constant/key.go
index bab746aaf..f1a1e9be0 100644
--- a/common/constant/key.go
+++ b/common/constant/key.go
@@ -157,6 +157,8 @@ const (
CycleReportKey = "cycle.report"
DefaultBlackListRecoverBlock = 16
CallTypeKey = "call-type"
+ ResponseHeaderKey = "response-header"
+ ResponseTrailerKey = "response-trailer"
CallUnary = "unary"
CallClientStream = "client-stream"
CallServerStream = "server-stream"
diff --git a/protocol/triple/client.go b/protocol/triple/client.go
index 1e548d2e1..6360972dd 100644
--- a/protocol/triple/client.go
+++ b/protocol/triple/client.go
@@ -66,13 +66,17 @@ type clientManager struct {
// TODO: code a triple client between clientManager and triple_protocol client
// TODO: write a NewClient for triple client
-func (cm *clientManager) callUnary(ctx context.Context, method string, req,
resp any) error {
+func (cm *clientManager) callUnary(ctx context.Context, method string, req,
resp any, responseHeader, responseTrailer *http.Header) error {
triReq := tri.NewRequest(req)
triResp := tri.NewResponse(resp)
- if err := cm.triClient.CallUnary(ctx, triReq, method, triResp); err !=
nil {
- return err
+ err := cm.triClient.CallUnary(ctx, triReq, method, triResp)
+ if responseHeader != nil {
+ *responseHeader = triResp.Header().Clone()
}
- return nil
+ if responseTrailer != nil {
+ *responseTrailer = triResp.Trailer().Clone()
+ }
+ return err
}
func (cm *clientManager) callClientStream(ctx context.Context, method string)
(any, error) {
diff --git a/protocol/triple/client_test.go b/protocol/triple/client_test.go
index 522958b71..277180bc5 100644
--- a/protocol/triple/client_test.go
+++ b/protocol/triple/client_test.go
@@ -20,6 +20,7 @@ package triple
import (
"context"
"net/http"
+ "net/http/httptest"
"testing"
"time"
)
@@ -27,6 +28,8 @@ import (
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+
+ "google.golang.org/protobuf/types/known/emptypb"
)
import (
@@ -103,6 +106,92 @@ func TestClientManager_Close(t *testing.T) {
require.NoError(t, err)
}
+func TestClientManagerCallUnaryCopiesResponseMetadata(t *testing.T) {
+ const (
+ responseHeaderKey = "x-test-response-header"
+ responseHeaderVal = "response-header-value"
+ responseTrailerKey = "x-test-response-trailer"
+ responseTrailerVal = "response-trailer-value"
+ )
+
+ handler := tri.NewUnaryHandler(
+ "/test.Service/Check",
+ func() any {
+ return new(emptypb.Empty)
+ },
+ func(ctx context.Context, req *tri.Request) (*tri.Response,
error) {
+ resp := tri.NewResponse(new(emptypb.Empty))
+ resp.Header().Set(responseHeaderKey, responseHeaderVal)
+ resp.Trailer().Set(responseTrailerKey,
responseTrailerVal)
+ return resp, nil
+ },
+ )
+ server := httptest.NewServer(handler)
+ t.Cleanup(server.Close)
+
+ cm := &clientManager{
+ isIDL: true,
+ triClient: tri.NewClient(server.Client(),
server.URL+"/test.Service", tri.WithTriple()),
+ }
+
+ var resp emptypb.Empty
+ var responseHeader http.Header
+ var responseTrailer http.Header
+ err := cm.callUnary(
+ context.Background(),
+ "Check",
+ &emptypb.Empty{},
+ &resp,
+ &responseHeader,
+ &responseTrailer,
+ )
+ require.NoError(t, err)
+ require.Equal(t, responseHeaderVal,
responseHeader.Get(responseHeaderKey))
+ require.Equal(t, responseTrailerVal,
responseTrailer.Get(responseTrailerKey))
+}
+
+func TestClientManagerCallUnaryCopiesErrorResponseMetadata(t *testing.T) {
+ const (
+ responseHeaderKey = "x-test-error-header"
+ responseHeaderVal = "error-header-value"
+ responseTrailerKey = "x-test-error-trailer"
+ responseTrailerVal = "error-trailer-value"
+ )
+
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r
*http.Request) {
+ w.Header().Set("Content-Type", "application/grpc")
+ w.Header().Set(responseHeaderKey, responseHeaderVal)
+ w.Header().Set("Trailer", "Grpc-Status, Grpc-Message,
"+responseTrailerKey)
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Grpc-Status", "3")
+ w.Header().Set("Grpc-Message", "invalid%20test%20request")
+ w.Header().Set(responseTrailerKey, responseTrailerVal)
+ })
+ server := httptest.NewServer(handler)
+ t.Cleanup(server.Close)
+
+ cm := &clientManager{
+ isIDL: true,
+ triClient: tri.NewClient(server.Client(),
server.URL+"/test.Service"),
+ }
+
+ var resp emptypb.Empty
+ var responseHeader http.Header
+ var responseTrailer http.Header
+ err := cm.callUnary(
+ context.Background(),
+ "Check",
+ &emptypb.Empty{},
+ &resp,
+ &responseHeader,
+ &responseTrailer,
+ )
+ require.Error(t, err)
+ require.Equal(t, tri.CodeInvalidArgument, tri.CodeOf(err))
+ require.Equal(t, responseHeaderVal,
responseHeader.Get(responseHeaderKey))
+ require.Equal(t, responseTrailerVal,
responseTrailer.Get(responseTrailerKey))
+}
+
// TestClientManager_CallMethods_MissingClient removed - no longer applicable
// in the service-level client architecture where all methods share a single
triClient.
diff --git a/protocol/triple/triple_invoker.go
b/protocol/triple/triple_invoker.go
index 4eb7a4ff1..d41a4fd45 100644
--- a/protocol/triple/triple_invoker.go
+++ b/protocol/triple/triple_invoker.go
@@ -101,11 +101,13 @@ func (ti *TripleInvoker) Invoke(ctx context.Context,
invocation base.Invocation)
inRawLen := len(inRaw)
+ responseHeader, responseTrailer := responseMetadataTargets(invocation)
+
if !ti.clientManager.isIDL {
switch callType {
case constant.CallUnary:
// todo(DMwangnima): consider inRawLen == 0
- if err := ti.clientManager.callUnary(ctx, method,
inRaw[0:inRawLen-1], inRaw[inRawLen-1]); err != nil {
+ if err := ti.clientManager.callUnary(ctx, method,
inRaw[0:inRawLen-1], inRaw[inRawLen-1], responseHeader, responseTrailer); err
!= nil {
result.SetError(err)
}
default:
@@ -118,7 +120,7 @@ func (ti *TripleInvoker) Invoke(ctx context.Context,
invocation base.Invocation)
if len(inRaw) != 2 {
panic(fmt.Sprintf("Wrong parameter Values number for
CallUnary, want 2, but got %d", inRawLen))
}
- if err := ti.clientManager.callUnary(ctx, method, inRaw[0],
inRaw[1]); err != nil {
+ if err := ti.clientManager.callUnary(ctx, method, inRaw[0],
inRaw[1], responseHeader, responseTrailer); err != nil {
result.SetError(err)
}
case constant.CallClientStream:
@@ -158,6 +160,28 @@ func (ti *TripleInvoker) Invoke(ctx context.Context,
invocation base.Invocation)
return &result
}
+func responseMetadataTargets(invocation base.Invocation) (*http.Header,
*http.Header) {
+ var responseHeader *http.Header
+ if headerRaw, ok :=
invocation.GetAttribute(constant.ResponseHeaderKey); ok {
+ if header, ok := headerRaw.(*http.Header); ok {
+ responseHeader = header
+ } else if headerRaw != nil {
+ logger.Warnf("[Triple][Invoker] invocation attribute %s
should be *http.Header, got %T", constant.ResponseHeaderKey, headerRaw)
+ }
+ }
+
+ var responseTrailer *http.Header
+ if trailerRaw, ok :=
invocation.GetAttribute(constant.ResponseTrailerKey); ok {
+ if trailer, ok := trailerRaw.(*http.Header); ok {
+ responseTrailer = trailer
+ } else if trailerRaw != nil {
+ logger.Warnf("[Triple][Invoker] invocation attribute %s
should be *http.Header, got %T", constant.ResponseTrailerKey, trailerRaw)
+ }
+ }
+
+ return responseHeader, responseTrailer
+}
+
func mergeAttachmentToOutgoing(ctx context.Context, inv base.Invocation)
(context.Context, error) {
// Todo(finalt) Temporarily solve the problem that the timeout time is
not valid
if timeout, ok := inv.GetAttachment(constant.TimeoutKey); ok {
diff --git a/protocol/triple/triple_protocol/triple.go
b/protocol/triple/triple_protocol/triple.go
index 99b5bf642..976437bc0 100644
--- a/protocol/triple/triple_protocol/triple.go
+++ b/protocol/triple/triple_protocol/triple.go
@@ -346,6 +346,10 @@ func receiveUnaryResponse(conn StreamingClientConn,
response AnyResponse) error
if !ok {
panic(fmt.Sprintf("response %T is not of Response type",
response))
}
+ defer func() {
+ resp.header = conn.ResponseHeader()
+ resp.trailer = conn.ResponseTrailer()
+ }()
if err := conn.Receive(resp.Msg); err != nil {
return err
}
@@ -357,8 +361,6 @@ func receiveUnaryResponse(conn StreamingClientConn,
response AnyResponse) error
} else if !errors.Is(err, io.EOF) {
return NewError(CodeUnknown, err)
}
- resp.header = conn.ResponseHeader()
- resp.trailer = conn.ResponseTrailer()
return nil
}