This is an automated email from the ASF dual-hosted git repository.

mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new 6262b0b3 Add request timeout (#927)
6262b0b3 is described below

commit 6262b0b38d3f269acd629e124066997d3122af9d
Author: mfordjody <[email protected]>
AuthorDate: Sun Jun 28 21:13:15 2026 +0800

    Add request timeout (#927)
---
 dubbod/discovery/cmd/app/cmd.go                    |   3 +
 dubbod/discovery/cmd/app/testproto/xds_test.pb.go  | 255 +++++++++++++++++++++
 dubbod/discovery/cmd/app/testproto/xds_test.proto  |  26 +++
 .../cmd/app/testproto/xds_test_grpc.pb.go          | 121 ++++++++++
 dubbod/discovery/cmd/app/xds_client.go             |  83 +++++--
 dubbod/discovery/cmd/app/xds_client_test.go        |  72 +++++-
 dubbod/discovery/cmd/app/xds_test_grpc.go          | 191 +++++++++++++++
 dubbod/discovery/pkg/networking/grpcgen/rds.go     |  31 ++-
 .../discovery/pkg/networking/grpcgen/rds_test.go   |  27 +++
 go.mod                                             |   2 +-
 go.sum                                             |   6 +-
 11 files changed, 787 insertions(+), 30 deletions(-)

diff --git a/dubbod/discovery/cmd/app/cmd.go b/dubbod/discovery/cmd/app/cmd.go
index 4ab203fd..3a5dbe09 100644
--- a/dubbod/discovery/cmd/app/cmd.go
+++ b/dubbod/discovery/cmd/app/cmd.go
@@ -93,6 +93,9 @@ func newExecuteCommand() *cobra.Command {
                        if err := discoveryServer.Start(stop); err != nil {
                                return fmt.Errorf("failed to start discovery 
service: %v", err)
                        }
+                       if err := startXDSTestGRPCServer(stop); err != nil {
+                               return fmt.Errorf("failed to start xDS test 
gRPC server: %v", err)
+                       }
 
                        // Wait for signal - when received, immediately exit
                        cmd.WaitSignal(stop)
diff --git a/dubbod/discovery/cmd/app/testproto/xds_test.pb.go 
b/dubbod/discovery/cmd/app/testproto/xds_test.pb.go
new file mode 100644
index 00000000..98f96d4e
--- /dev/null
+++ b/dubbod/discovery/cmd/app/testproto/xds_test.pb.go
@@ -0,0 +1,255 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+//     protoc-gen-go v1.36.11
+//     protoc        v6.33.0
+// source: dubbod/discovery/cmd/app/testproto/xds_test.proto
+
+package testproto
+
+import (
+       protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+       protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+       reflect "reflect"
+       sync "sync"
+       unsafe "unsafe"
+)
+
+const (
+       // Verify that this generated code is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+       // Verify that runtime/protoimpl is sufficiently up-to-date.
+       _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type ForwardHTTPRequest struct {
+       state           protoimpl.MessageState `protogen:"open.v1"`
+       Url             string                 
`protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
+       Path            string                 
`protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"`
+       Count           int32                  
`protobuf:"varint,3,opt,name=count,proto3" json:"count,omitempty"`
+       Headers         []string               
`protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"`
+       RequestInterval string                 
`protobuf:"bytes,5,opt,name=request_interval,json=requestInterval,proto3" 
json:"request_interval,omitempty"`
+       Expect          string                 
`protobuf:"bytes,6,opt,name=expect,proto3" json:"expect,omitempty"`
+       RequestTimeout  string                 
`protobuf:"bytes,7,opt,name=request_timeout,json=requestTimeout,proto3" 
json:"request_timeout,omitempty"`
+       WaitTimeout     string                 
`protobuf:"bytes,8,opt,name=wait_timeout,json=waitTimeout,proto3" 
json:"wait_timeout,omitempty"`
+       Namespace       string                 
`protobuf:"bytes,9,opt,name=namespace,proto3" json:"namespace,omitempty"`
+       ServiceAccount  string                 
`protobuf:"bytes,10,opt,name=service_account,json=serviceAccount,proto3" 
json:"service_account,omitempty"`
+       unknownFields   protoimpl.UnknownFields
+       sizeCache       protoimpl.SizeCache
+}
+
+func (x *ForwardHTTPRequest) Reset() {
+       *x = ForwardHTTPRequest{}
+       mi := 
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[0]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *ForwardHTTPRequest) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ForwardHTTPRequest) ProtoMessage() {}
+
+func (x *ForwardHTTPRequest) ProtoReflect() protoreflect.Message {
+       mi := 
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[0]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use ForwardHTTPRequest.ProtoReflect.Descriptor instead.
+func (*ForwardHTTPRequest) Descriptor() ([]byte, []int) {
+       return 
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ForwardHTTPRequest) GetUrl() string {
+       if x != nil {
+               return x.Url
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetPath() string {
+       if x != nil {
+               return x.Path
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetCount() int32 {
+       if x != nil {
+               return x.Count
+       }
+       return 0
+}
+
+func (x *ForwardHTTPRequest) GetHeaders() []string {
+       if x != nil {
+               return x.Headers
+       }
+       return nil
+}
+
+func (x *ForwardHTTPRequest) GetRequestInterval() string {
+       if x != nil {
+               return x.RequestInterval
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetExpect() string {
+       if x != nil {
+               return x.Expect
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetRequestTimeout() string {
+       if x != nil {
+               return x.RequestTimeout
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetWaitTimeout() string {
+       if x != nil {
+               return x.WaitTimeout
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetNamespace() string {
+       if x != nil {
+               return x.Namespace
+       }
+       return ""
+}
+
+func (x *ForwardHTTPRequest) GetServiceAccount() string {
+       if x != nil {
+               return x.ServiceAccount
+       }
+       return ""
+}
+
+type ForwardHTTPResponse struct {
+       state         protoimpl.MessageState `protogen:"open.v1"`
+       Output        []string               
`protobuf:"bytes,1,rep,name=output,proto3" json:"output,omitempty"`
+       unknownFields protoimpl.UnknownFields
+       sizeCache     protoimpl.SizeCache
+}
+
+func (x *ForwardHTTPResponse) Reset() {
+       *x = ForwardHTTPResponse{}
+       mi := 
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[1]
+       ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+       ms.StoreMessageInfo(mi)
+}
+
+func (x *ForwardHTTPResponse) String() string {
+       return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ForwardHTTPResponse) ProtoMessage() {}
+
+func (x *ForwardHTTPResponse) ProtoReflect() protoreflect.Message {
+       mi := 
&file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes[1]
+       if x != nil {
+               ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+               if ms.LoadMessageInfo() == nil {
+                       ms.StoreMessageInfo(mi)
+               }
+               return ms
+       }
+       return mi.MessageOf(x)
+}
+
+// Deprecated: Use ForwardHTTPResponse.ProtoReflect.Descriptor instead.
+func (*ForwardHTTPResponse) Descriptor() ([]byte, []int) {
+       return 
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ForwardHTTPResponse) GetOutput() []string {
+       if x != nil {
+               return x.Output
+       }
+       return nil
+}
+
+var File_dubbod_discovery_cmd_app_testproto_xds_test_proto 
protoreflect.FileDescriptor
+
+const file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc = "" +
+       "\n" +
+       
"1dubbod/discovery/cmd/app/testproto/xds_test.proto\x12\x05proto\"\xc0\x02\n" +
+       "\x12ForwardHTTPRequest\x12\x10\n" +
+       "\x03url\x18\x01 \x01(\tR\x03url\x12\x12\n" +
+       "\x04path\x18\x02 \x01(\tR\x04path\x12\x14\n" +
+       "\x05count\x18\x03 \x01(\x05R\x05count\x12\x18\n" +
+       "\aheaders\x18\x04 \x03(\tR\aheaders\x12)\n" +
+       "\x10request_interval\x18\x05 \x01(\tR\x0frequestInterval\x12\x16\n" +
+       "\x06expect\x18\x06 \x01(\tR\x06expect\x12'\n" +
+       "\x0frequest_timeout\x18\a \x01(\tR\x0erequestTimeout\x12!\n" +
+       "\fwait_timeout\x18\b \x01(\tR\vwaitTimeout\x12\x1c\n" +
+       "\tnamespace\x18\t \x01(\tR\tnamespace\x12'\n" +
+       "\x0fservice_account\x18\n" +
+       " \x01(\tR\x0eserviceAccount\"-\n" +
+       "\x13ForwardHTTPResponse\x12\x16\n" +
+       "\x06output\x18\x01 \x03(\tR\x06output2V\n" +
+       "\x0eXDSTestService\x12D\n" +
+       
"\vForwardHTTP\x12\x19.proto.ForwardHTTPRequest\x1a\x1a.proto.ForwardHTTPResponseBQZOgithub.com/apache/dubbo-kubernetes/dubbod/discovery/cmd/app/testproto;testprotob\x06proto3"
+
+var (
+       file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescOnce 
sync.Once
+       file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescData 
[]byte
+)
+
+func file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescGZIP() 
[]byte {
+       
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescOnce.Do(func() {
+               
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescData = 
protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc),
 len(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc)))
+       })
+       return 
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDescData
+}
+
+var file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes = 
make([]protoimpl.MessageInfo, 2)
+var file_dubbod_discovery_cmd_app_testproto_xds_test_proto_goTypes = []any{
+       (*ForwardHTTPRequest)(nil),  // 0: proto.ForwardHTTPRequest
+       (*ForwardHTTPResponse)(nil), // 1: proto.ForwardHTTPResponse
+}
+var file_dubbod_discovery_cmd_app_testproto_xds_test_proto_depIdxs = []int32{
+       0, // 0: proto.XDSTestService.ForwardHTTP:input_type -> 
proto.ForwardHTTPRequest
+       1, // 1: proto.XDSTestService.ForwardHTTP:output_type -> 
proto.ForwardHTTPResponse
+       1, // [1:2] is the sub-list for method output_type
+       0, // [0:1] is the sub-list for method input_type
+       0, // [0:0] is the sub-list for extension type_name
+       0, // [0:0] is the sub-list for extension extendee
+       0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_dubbod_discovery_cmd_app_testproto_xds_test_proto_init() }
+func file_dubbod_discovery_cmd_app_testproto_xds_test_proto_init() {
+       if File_dubbod_discovery_cmd_app_testproto_xds_test_proto != nil {
+               return
+       }
+       type x struct{}
+       out := protoimpl.TypeBuilder{
+               File: protoimpl.DescBuilder{
+                       GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+                       RawDescriptor: 
unsafe.Slice(unsafe.StringData(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc),
 len(file_dubbod_discovery_cmd_app_testproto_xds_test_proto_rawDesc)),
+                       NumEnums:      0,
+                       NumMessages:   2,
+                       NumExtensions: 0,
+                       NumServices:   1,
+               },
+               GoTypes:           
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_goTypes,
+               DependencyIndexes: 
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_depIdxs,
+               MessageInfos:      
file_dubbod_discovery_cmd_app_testproto_xds_test_proto_msgTypes,
+       }.Build()
+       File_dubbod_discovery_cmd_app_testproto_xds_test_proto = out.File
+       file_dubbod_discovery_cmd_app_testproto_xds_test_proto_goTypes = nil
+       file_dubbod_discovery_cmd_app_testproto_xds_test_proto_depIdxs = nil
+}
diff --git a/dubbod/discovery/cmd/app/testproto/xds_test.proto 
b/dubbod/discovery/cmd/app/testproto/xds_test.proto
new file mode 100644
index 00000000..73b18b52
--- /dev/null
+++ b/dubbod/discovery/cmd/app/testproto/xds_test.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package proto;
+
+option go_package = 
"github.com/apache/dubbo-kubernetes/dubbod/discovery/cmd/app/testproto;testproto";
+
+service XDSTestService {
+  rpc ForwardHTTP(ForwardHTTPRequest) returns (ForwardHTTPResponse);
+}
+
+message ForwardHTTPRequest {
+  string url = 1;
+  string path = 2;
+  int32 count = 3;
+  repeated string headers = 4;
+  string request_interval = 5;
+  string expect = 6;
+  string request_timeout = 7;
+  string wait_timeout = 8;
+  string namespace = 9;
+  string service_account = 10;
+}
+
+message ForwardHTTPResponse {
+  repeated string output = 1;
+}
diff --git a/dubbod/discovery/cmd/app/testproto/xds_test_grpc.pb.go 
b/dubbod/discovery/cmd/app/testproto/xds_test_grpc.pb.go
new file mode 100644
index 00000000..22881a7a
--- /dev/null
+++ b/dubbod/discovery/cmd/app/testproto/xds_test_grpc.pb.go
@@ -0,0 +1,121 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.6.1
+// - protoc             v6.33.0
+// source: dubbod/discovery/cmd/app/testproto/xds_test.proto
+
+package testproto
+
+import (
+       context "context"
+       grpc "google.golang.org/grpc"
+       codes "google.golang.org/grpc/codes"
+       status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+       XDSTestService_ForwardHTTP_FullMethodName = 
"/proto.XDSTestService/ForwardHTTP"
+)
+
+// XDSTestServiceClient is the client API for XDSTestService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please 
refer to 
https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type XDSTestServiceClient interface {
+       ForwardHTTP(ctx context.Context, in *ForwardHTTPRequest, opts 
...grpc.CallOption) (*ForwardHTTPResponse, error)
+}
+
+type xDSTestServiceClient struct {
+       cc grpc.ClientConnInterface
+}
+
+func NewXDSTestServiceClient(cc grpc.ClientConnInterface) XDSTestServiceClient 
{
+       return &xDSTestServiceClient{cc}
+}
+
+func (c *xDSTestServiceClient) ForwardHTTP(ctx context.Context, in 
*ForwardHTTPRequest, opts ...grpc.CallOption) (*ForwardHTTPResponse, error) {
+       cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+       out := new(ForwardHTTPResponse)
+       err := c.cc.Invoke(ctx, XDSTestService_ForwardHTTP_FullMethodName, in, 
out, cOpts...)
+       if err != nil {
+               return nil, err
+       }
+       return out, nil
+}
+
+// XDSTestServiceServer is the server API for XDSTestService service.
+// All implementations must embed UnimplementedXDSTestServiceServer
+// for forward compatibility.
+type XDSTestServiceServer interface {
+       ForwardHTTP(context.Context, *ForwardHTTPRequest) 
(*ForwardHTTPResponse, error)
+       mustEmbedUnimplementedXDSTestServiceServer()
+}
+
+// UnimplementedXDSTestServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedXDSTestServiceServer struct{}
+
+func (UnimplementedXDSTestServiceServer) ForwardHTTP(context.Context, 
*ForwardHTTPRequest) (*ForwardHTTPResponse, error) {
+       return nil, status.Error(codes.Unimplemented, "method ForwardHTTP not 
implemented")
+}
+func (UnimplementedXDSTestServiceServer) 
mustEmbedUnimplementedXDSTestServiceServer() {}
+func (UnimplementedXDSTestServiceServer) testEmbeddedByValue()                 
       {}
+
+// UnsafeXDSTestServiceServer may be embedded to opt out of forward 
compatibility for this service.
+// Use of this interface is not recommended, as added methods to 
XDSTestServiceServer will
+// result in compilation errors.
+type UnsafeXDSTestServiceServer interface {
+       mustEmbedUnimplementedXDSTestServiceServer()
+}
+
+func RegisterXDSTestServiceServer(s grpc.ServiceRegistrar, srv 
XDSTestServiceServer) {
+       // If the following call panics, it indicates 
UnimplementedXDSTestServiceServer was
+       // embedded by pointer and is nil.  This will cause panics if an
+       // unimplemented method is ever invoked, so we test this at 
initialization
+       // time to prevent it from happening at runtime later due to I/O.
+       if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+               t.testEmbeddedByValue()
+       }
+       s.RegisterService(&XDSTestService_ServiceDesc, srv)
+}
+
+func _XDSTestService_ForwardHTTP_Handler(srv interface{}, ctx context.Context, 
dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) 
(interface{}, error) {
+       in := new(ForwardHTTPRequest)
+       if err := dec(in); err != nil {
+               return nil, err
+       }
+       if interceptor == nil {
+               return srv.(XDSTestServiceServer).ForwardHTTP(ctx, in)
+       }
+       info := &grpc.UnaryServerInfo{
+               Server:     srv,
+               FullMethod: XDSTestService_ForwardHTTP_FullMethodName,
+       }
+       handler := func(ctx context.Context, req interface{}) (interface{}, 
error) {
+               return srv.(XDSTestServiceServer).ForwardHTTP(ctx, 
req.(*ForwardHTTPRequest))
+       }
+       return interceptor(ctx, in, info, handler)
+}
+
+// XDSTestService_ServiceDesc is the grpc.ServiceDesc for XDSTestService 
service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var XDSTestService_ServiceDesc = grpc.ServiceDesc{
+       ServiceName: "proto.XDSTestService",
+       HandlerType: (*XDSTestServiceServer)(nil),
+       Methods: []grpc.MethodDesc{
+               {
+                       MethodName: "ForwardHTTP",
+                       Handler:    _XDSTestService_ForwardHTTP_Handler,
+               },
+       },
+       Streams:  []grpc.StreamDesc{},
+       Metadata: "dubbod/discovery/cmd/app/testproto/xds_test.proto",
+}
diff --git a/dubbod/discovery/cmd/app/xds_client.go 
b/dubbod/discovery/cmd/app/xds_client.go
index 57443df1..02e68c72 100644
--- a/dubbod/discovery/cmd/app/xds_client.go
+++ b/dubbod/discovery/cmd/app/xds_client.go
@@ -100,6 +100,7 @@ type xdsDestination struct {
 type xdsRouteSnapshot struct {
        Host         string           `json:"host"`
        Port         int              `json:"port"`
+       Timeout      string           `json:"timeout,omitempty"`
        Destinations []xdsDestination `json:"destinations"`
 }
 
@@ -116,6 +117,7 @@ type sampleADSClient struct {
        mu            sync.RWMutex
        subs          map[string][]string
        route         map[string]uint32
+       routeTimeout  time.Duration
        endpoints     map[string][]xdsEndpoint
        clusterTLS    map[string]*tlsv1.UpstreamTlsContext
        updates       chan struct{}
@@ -425,12 +427,13 @@ func (c *sampleADSClient) handleResponse(resp 
*discovery.DiscoveryResponse) erro
                        return c.subscribe(v1.RouteType, routeNames)
                }
        case v1.RouteType:
-               weights, clusters, err := 
routeWeightsFromRoutes(resp.Resources, c.path, c.requestHeaders)
+               weights, clusters, timeout, err := 
routeWeightsFromRoutes(resp.Resources, c.path, c.requestHeaders)
                if err != nil {
                        return err
                }
                c.mu.Lock()
                c.route = weights
+               c.routeTimeout = timeout
                c.mu.Unlock()
                c.notify()
                if len(clusters) > 0 {
@@ -506,6 +509,9 @@ func (c *sampleADSClient) readySnapshot(expected 
map[string]uint32) (xdsRouteSna
        c.mu.RLock()
        defer c.mu.RUnlock()
        snapshot := xdsRouteSnapshot{Host: c.host, Port: c.port}
+       if c.routeTimeout > 0 {
+               snapshot.Timeout = c.routeTimeout.String()
+       }
        for clusterName, weight := range c.route {
                if weight == 0 {
                        continue
@@ -564,24 +570,32 @@ func routeNamesFromListeners(resources []*anypb.Any) 
([]string, error) {
        return sortedUnique(out), nil
 }
 
-func routeWeightsFromRoutes(resources []*anypb.Any, requestPath string, 
requestHeaders http.Header) (map[string]uint32, []string, error) {
+func routeWeightsFromRoutes(resources []*anypb.Any, requestPath string, 
requestHeaders http.Header) (map[string]uint32, []string, time.Duration, error) 
{
        weights := map[string]uint32{}
        for _, resource := range resources {
                rc := &routev1.RouteConfiguration{}
                if err := proto.Unmarshal(resource.Value, rc); err != nil {
-                       return nil, nil, err
+                       return nil, nil, 0, err
                }
                for _, vh := range rc.GetVirtualHosts() {
                        for _, rt := range vh.GetRoutes() {
                                if !routeMatchesRequest(rt.GetMatch(), 
requestPath, requestHeaders) {
                                        continue
                                }
-                               addRouteActionWeights(weights, rt.GetRoute())
-                               return weights, 
sortedWeightClusterNames(weights), nil
+                               action := rt.GetRoute()
+                               addRouteActionWeights(weights, action)
+                               return weights, 
sortedWeightClusterNames(weights), routeActionTimeout(action), nil
                        }
                }
        }
-       return weights, sortedWeightClusterNames(weights), nil
+       return weights, sortedWeightClusterNames(weights), 0, nil
+}
+
+func routeActionTimeout(action *routev1.RouteAction) time.Duration {
+       if action == nil || action.GetTimeout() == nil {
+               return 0
+       }
+       return action.GetTimeout().AsDuration()
 }
 
 func addRouteActionWeights(weights map[string]uint32, action 
*routev1.RouteAction) {
@@ -749,12 +763,18 @@ func destinationFromCluster(clusterName string, weight 
uint32, endpoints []xdsEn
 }
 
 func runSampleRequests(ctx context.Context, adsClient *sampleADSClient, 
snapshot xdsRouteSnapshot, count int, requestInterval, requestTimeout 
time.Duration) error {
+       _, err := runSampleRequestsWithOutput(ctx, adsClient, snapshot, count, 
requestInterval, requestTimeout, os.Stdout)
+       return err
+}
+
+func runSampleRequestsWithOutput(ctx context.Context, adsClient 
*sampleADSClient, snapshot xdsRouteSnapshot, count int, requestInterval, 
requestTimeout time.Duration, writer io.Writer) ([]string, error) {
        picker, err := newSmoothWeightedPicker(snapshot)
        if err != nil {
-               return err
+               return nil, err
        }
        currentSignature := snapshotSignature(snapshot)
        clients := newSampleRequestClients(adsClient, requestTimeout)
+       output := make([]string, 0, count)
        for i := 0; i < count; i++ {
                if updated, ok := adsClient.readySnapshot(nil); ok {
                        updatedSignature := snapshotSignature(updated)
@@ -763,22 +783,28 @@ func runSampleRequests(ctx context.Context, adsClient 
*sampleADSClient, snapshot
                                currentSignature = updatedSignature
                                picker, err = newSmoothWeightedPicker(snapshot)
                                if err != nil {
-                                       return err
+                                       return nil, err
                                }
                        }
                }
                destination, endpoint, err := picker.Next()
                if err != nil {
-                       return err
+                       return nil, err
                }
                httpClient, scheme, err := 
clients.clientForDestination(destination)
                if err != nil {
-                       return err
+                       return nil, err
                }
-               req, err := http.NewRequestWithContext(ctx, http.MethodGet,
+               requestCtx := ctx
+               cancel := func() {}
+               if timeout, ok := routeTimeoutDuration(snapshot.Timeout); ok {
+                       requestCtx, cancel = context.WithTimeout(ctx, timeout)
+               }
+               req, err := http.NewRequestWithContext(requestCtx, 
http.MethodGet,
                        fmt.Sprintf("%s://%s%s", scheme, 
net.JoinHostPort(endpoint.Address, strconv.Itoa(int(endpoint.Port))), 
adsClient.path), nil)
                if err != nil {
-                       return err
+                       cancel()
+                       return nil, err
                }
                req.Host = snapshot.Host
                for name, values := range adsClient.requestHeaders {
@@ -788,14 +814,20 @@ func runSampleRequests(ctx context.Context, adsClient 
*sampleADSClient, snapshot
                }
                resp, err := httpClient.Do(req)
                if err != nil {
-                       return err
+                       cancel()
+                       return nil, err
                }
                body, readErr := io.ReadAll(resp.Body)
                _ = resp.Body.Close()
+               cancel()
                if readErr != nil {
-                       return readErr
+                       return nil, readErr
+               }
+               line := strings.TrimSpace(string(body))
+               output = append(output, line+"\n")
+               if writer != nil {
+                       fmt.Fprintln(writer, line)
                }
-               fmt.Println(strings.TrimSpace(string(body)))
                if requestInterval > 0 && i+1 < count {
                        timer := time.NewTimer(requestInterval)
                        select {
@@ -803,12 +835,23 @@ func runSampleRequests(ctx context.Context, adsClient 
*sampleADSClient, snapshot
                                if !timer.Stop() {
                                        <-timer.C
                                }
-                               return ctx.Err()
+                               return output, ctx.Err()
                        case <-timer.C:
                        }
                }
        }
-       return nil
+       return output, nil
+}
+
+func routeTimeoutDuration(value string) (time.Duration, bool) {
+       if value == "" {
+               return 0, false
+       }
+       timeout, err := time.ParseDuration(value)
+       if err != nil || timeout <= 0 {
+               return 0, false
+       }
+       return timeout, true
 }
 
 type sampleRequestClients struct {
@@ -1033,7 +1076,11 @@ func routeSummary(snapshot xdsRouteSnapshot) string {
        for _, dest := range snapshot.Destinations {
                parts = append(parts, fmt.Sprintf("%s=%d endpoints=%d", 
destinationWeightKey(dest), dest.Weight, len(dest.Endpoints)))
        }
-       return fmt.Sprintf("%s:%d %s", snapshot.Host, snapshot.Port, 
strings.Join(parts, ","))
+       timeout := ""
+       if snapshot.Timeout != "" {
+               timeout = " timeout=" + snapshot.Timeout
+       }
+       return fmt.Sprintf("%s:%d%s %s", snapshot.Host, snapshot.Port, timeout, 
strings.Join(parts, ","))
 }
 
 func sortedUnique(values []string) []string {
diff --git a/dubbod/discovery/cmd/app/xds_client_test.go 
b/dubbod/discovery/cmd/app/xds_client_test.go
index 7b759ede..4bf5d64e 100644
--- a/dubbod/discovery/cmd/app/xds_client_test.go
+++ b/dubbod/discovery/cmd/app/xds_client_test.go
@@ -17,6 +17,7 @@ import (
        tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
        routev1 "github.com/kdubbo/xds-api/route/v1"
        "google.golang.org/protobuf/types/known/anypb"
+       "google.golang.org/protobuf/types/known/durationpb"
        "google.golang.org/protobuf/types/known/wrapperspb"
 )
 
@@ -245,6 +246,41 @@ func TestRunSampleRequestsUsesRequestPathAndHeaders(t 
*testing.T) {
        }
 }
 
+func TestRunSampleRequestsUsesRouteTimeout(t *testing.T) {
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               time.Sleep(50 * time.Millisecond)
+               _, _ = w.Write([]byte("late"))
+       }))
+       defer server.Close()
+
+       endpoint := endpointForServer(t, server)
+       snapshot := xdsRouteSnapshot{
+               Host:    "reviews.moviereview.svc.cluster.local",
+               Port:    9080,
+               Timeout: "1ms",
+               Destinations: []xdsDestination{{
+                       Cluster:   
"outbound|9080|v1|reviews.moviereview.svc.cluster.local",
+                       Host:      "reviews.moviereview.svc.cluster.local",
+                       Subset:    "v1",
+                       Weight:    100,
+                       Endpoints: []xdsEndpoint{endpoint},
+               }},
+       }
+       client := &sampleADSClient{
+               host:         snapshot.Host,
+               port:         snapshot.Port,
+               path:         "/",
+               route:        
map[string]uint32{snapshot.Destinations[0].Cluster: 100},
+               routeTimeout: time.Millisecond,
+               endpoints:    
map[string][]xdsEndpoint{snapshot.Destinations[0].Cluster: 
[]xdsEndpoint{endpoint}},
+       }
+
+       err := runSampleRequests(context.Background(), client, snapshot, 1, 0, 
time.Second)
+       if err == nil || !strings.Contains(err.Error(), "context deadline 
exceeded") {
+               t.Fatalf("runSampleRequests() error = %v, want context deadline 
exceeded", err)
+       }
+}
+
 func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t *testing.T) {
        resources := []*anypb.Any{mustAnyRouteConfig(t, 
&routev1.RouteConfiguration{
                VirtualHosts: []*routev1.VirtualHost{{
@@ -274,7 +310,7 @@ func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t 
*testing.T) {
                }},
        })}
 
-       weights, _, err := routeWeightsFromRoutes(resources, "/", 
http.Header{"End-User": []string{"terminal-user"}})
+       weights, _, _, err := routeWeightsFromRoutes(resources, "/", 
http.Header{"End-User": []string{"terminal-user"}})
        if err != nil {
                t.Fatalf("routeWeightsFromRoutes() error = %v", err)
        }
@@ -282,7 +318,7 @@ func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t 
*testing.T) {
                t.Fatalf("terminal-user weights = %v, want v1=100 only", 
weights)
        }
 
-       weights, _, err = routeWeightsFromRoutes(resources, "/", nil)
+       weights, _, _, err = routeWeightsFromRoutes(resources, "/", nil)
        if err != nil {
                t.Fatalf("routeWeightsFromRoutes() fallback error = %v", err)
        }
@@ -293,6 +329,38 @@ func TestRouteWeightsFromRoutesFiltersHeaderMatchedRoute(t 
*testing.T) {
        }
 }
 
+func TestRouteWeightsFromRoutesReadsRequestTimeout(t *testing.T) {
+       resources := []*anypb.Any{mustAnyRouteConfig(t, 
&routev1.RouteConfiguration{
+               VirtualHosts: []*routev1.VirtualHost{{
+                       Routes: []*routev1.Route{{
+                               Match: &routev1.RouteMatch{PathSpecifier: 
&routev1.RouteMatch_Prefix{Prefix: "/reviews"}},
+                               Action: &routev1.Route_Route{
+                                       Route: &routev1.RouteAction{
+                                               ClusterSpecifier: 
&routev1.RouteAction_WeightedClusters{
+                                                       WeightedClusters: 
&routev1.WeightedCluster{Clusters: []*routev1.WeightedCluster_ClusterWeight{{
+                                                               Name:   
"outbound|9080|v1|reviews.moviereview.svc.cluster.local",
+                                                               Weight: 
wrapperspb.UInt32(100),
+                                                       }}},
+                                               },
+                                               Timeout: durationpb.New(500 * 
time.Millisecond),
+                                       },
+                               },
+                       }},
+               }},
+       })}
+
+       weights, _, timeout, err := routeWeightsFromRoutes(resources, 
"/reviews", nil)
+       if err != nil {
+               t.Fatalf("routeWeightsFromRoutes() error = %v", err)
+       }
+       if got := 
weights["outbound|9080|v1|reviews.moviereview.svc.cluster.local"]; got != 100 {
+               t.Fatalf("weights = %v, want v1=100", weights)
+       }
+       if timeout != 500*time.Millisecond {
+               t.Fatalf("timeout = %v, want 500ms", timeout)
+       }
+}
+
 func TestSampleRequestClientsDoNotBypassMTLS(t *testing.T) {
        clients := newSampleRequestClients(&sampleADSClient{}, time.Second)
        destination := xdsDestination{
diff --git a/dubbod/discovery/cmd/app/xds_test_grpc.go 
b/dubbod/discovery/cmd/app/xds_test_grpc.go
new file mode 100644
index 00000000..b9732b54
--- /dev/null
+++ b/dubbod/discovery/cmd/app/xds_test_grpc.go
@@ -0,0 +1,191 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package app
+
+import (
+       "context"
+       "fmt"
+       "io"
+       "net"
+       neturl "net/url"
+       "os"
+       "strconv"
+       "strings"
+       "time"
+
+       testproto 
"github.com/apache/dubbo-kubernetes/dubbod/discovery/cmd/app/testproto"
+       "github.com/apache/dubbo-kubernetes/pkg/config/constants"
+       "github.com/apache/dubbo-kubernetes/pkg/log"
+       "google.golang.org/grpc"
+       "google.golang.org/grpc/codes"
+       "google.golang.org/grpc/reflection"
+       "google.golang.org/grpc/status"
+)
+
+const defaultXDSTestGRPCAddr = ":17171"
+
+type xdsTestGRPCServer struct {
+       testproto.UnimplementedXDSTestServiceServer
+}
+
+func startXDSTestGRPCServer(stop <-chan struct{}) error {
+       addr := firstNonEmpty(os.Getenv("XDS_TEST_GRPC_ADDR"), 
defaultXDSTestGRPCAddr)
+       if strings.EqualFold(addr, "disabled") {
+               return nil
+       }
+       listener, err := net.Listen("tcp", addr)
+       if err != nil {
+               return err
+       }
+       server := grpc.NewServer()
+       testproto.RegisterXDSTestServiceServer(server, &xdsTestGRPCServer{})
+       reflection.Register(server)
+       go func() {
+               <-stop
+               server.GracefulStop()
+       }()
+       go func() {
+               if err := server.Serve(listener); err != nil {
+                       log.Warnf("xDS test gRPC server stopped: %v", err)
+               }
+       }()
+       log.Infof("xDS test gRPC server listening on %s", addr)
+       return nil
+}
+
+func (s *xdsTestGRPCServer) ForwardHTTP(ctx context.Context, req 
*testproto.ForwardHTTPRequest) (*testproto.ForwardHTTPResponse, error) {
+       opts, err := xdsOptionsFromForwardHTTPRequest(req)
+       if err != nil {
+               return nil, status.Error(codes.InvalidArgument, err.Error())
+       }
+       expected, err := parseExpectedWeights(opts.expect)
+       if err != nil {
+               return nil, status.Error(codes.InvalidArgument, err.Error())
+       }
+       client, err := newSampleADSClient(ctx, opts)
+       if err != nil {
+               return nil, status.Error(codes.Unavailable, err.Error())
+       }
+       defer client.close()
+       if err := client.start(); err != nil {
+               return nil, status.Error(codes.Unavailable, err.Error())
+       }
+       snapshot, err := client.waitForRoute(ctx, expected, opts.waitTimeout)
+       if err != nil {
+               return nil, status.Error(codes.FailedPrecondition, err.Error())
+       }
+       output, err := runSampleRequestsWithOutput(ctx, client, snapshot, 
opts.count, opts.requestInterval, opts.requestTimeout, io.Discard)
+       if err != nil {
+               return nil, status.Error(codes.Unknown, err.Error())
+       }
+       return &testproto.ForwardHTTPResponse{Output: output}, nil
+}
+
+func xdsOptionsFromForwardHTTPRequest(req *testproto.ForwardHTTPRequest) 
(*xdsClientOptions, error) {
+       if req == nil {
+               return nil, fmt.Errorf("request is required")
+       }
+       host, port, err := parseXDSForwardURL(req.GetUrl())
+       if err != nil {
+               return nil, err
+       }
+       namespace := firstNonEmpty(req.GetNamespace(), 
namespaceFromServiceHost(host), os.Getenv("POD_NAMESPACE"), "default")
+       serviceAccount := firstNonEmpty(req.GetServiceAccount(), namespace)
+       requestHeaders, err := parseRequestHeaders(req.GetHeaders())
+       if err != nil {
+               return nil, err
+       }
+       requestInterval, err := durationField(req.GetRequestInterval(), 0)
+       if err != nil {
+               return nil, fmt.Errorf("invalid requestInterval: %w", err)
+       }
+       requestTimeout, err := durationField(req.GetRequestTimeout(), 
5*time.Second)
+       if err != nil {
+               return nil, fmt.Errorf("invalid requestTimeout: %w", err)
+       }
+       waitTimeout, err := durationField(req.GetWaitTimeout(), 30*time.Second)
+       if err != nil {
+               return nil, fmt.Errorf("invalid waitTimeout: %w", err)
+       }
+       count := int(req.GetCount())
+       if count <= 0 {
+               count = 1
+       }
+       return &xdsClientOptions{
+               host:            host,
+               port:            port,
+               path:            normalizeRequestPath(req.GetPath()),
+               requestHeaders:  requestHeaders,
+               target:          net.JoinHostPort(host, strconv.Itoa(port)),
+               xdsAddress:      
firstNonEmpty(os.Getenv("XDS_TEST_XDS_ADDRESS"), "127.0.0.1:26010"),
+               namespace:       namespace,
+               podName:         firstNonEmpty(os.Getenv("POD_NAME"), 
os.Getenv("HOSTNAME"), "xclient"),
+               podIP:           firstNonEmpty(os.Getenv("INSTANCE_IP"), 
os.Getenv("POD_IP"), "127.0.0.1"),
+               serviceAccount:  serviceAccount,
+               trustDomain:     firstNonEmpty(os.Getenv("TRUST_DOMAIN"), 
constants.DefaultClusterLocalDomain),
+               domainSuffix:    firstNonEmpty(os.Getenv("DOMAIN_SUFFIX"), 
constants.DefaultClusterLocalDomain),
+               clusterID:       
firstNonEmpty(os.Getenv("DUBBO_META_CLUSTER_ID"), 
string(constants.DefaultClusterName)),
+               count:           count,
+               expect:          req.GetExpect(),
+               waitTimeout:     waitTimeout,
+               requestInterval: requestInterval,
+               requestTimeout:  requestTimeout,
+               insecure:        true,
+       }, nil
+}
+
+func parseXDSForwardURL(raw string) (string, int, error) {
+       raw = strings.TrimSpace(raw)
+       if raw == "" {
+               return "", 0, fmt.Errorf("url is required")
+       }
+       parsed, err := neturl.Parse(raw)
+       if err != nil {
+               return "", 0, err
+       }
+       if parsed.Scheme != "xds" {
+               return "", 0, fmt.Errorf("unsupported url scheme %q, want xds", 
parsed.Scheme)
+       }
+       target := parsed.Host
+       if target == "" {
+               target = strings.TrimPrefix(parsed.Path, "/")
+       }
+       target = strings.TrimSuffix(target, "/")
+       host, portValue, err := net.SplitHostPort(target)
+       if err != nil {
+               return "", 0, fmt.Errorf("invalid xds url target %q: %w", 
target, err)
+       }
+       port, err := strconv.Atoi(portValue)
+       if err != nil {
+               return "", 0, fmt.Errorf("invalid xds url port %q: %w", 
portValue, err)
+       }
+       return host, port, nil
+}
+
+func namespaceFromServiceHost(host string) string {
+       parts := strings.Split(host, ".")
+       if len(parts) >= 4 && parts[2] == "svc" && parts[1] != "" {
+               return parts[1]
+       }
+       return ""
+}
+
+func durationField(value string, defaultValue time.Duration) (time.Duration, 
error) {
+       if strings.TrimSpace(value) == "" {
+               return defaultValue, nil
+       }
+       return time.ParseDuration(value)
+}
diff --git a/dubbod/discovery/pkg/networking/grpcgen/rds.go 
b/dubbod/discovery/pkg/networking/grpcgen/rds.go
index 55d3f778..249ab63a 100644
--- a/dubbod/discovery/pkg/networking/grpcgen/rds.go
+++ b/dubbod/discovery/pkg/networking/grpcgen/rds.go
@@ -20,6 +20,7 @@ import (
        "fmt"
        "strconv"
        "strings"
+       "time"
 
        "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/util/protoconv"
        discovery "github.com/kdubbo/xds-api/service/discovery/v1"
@@ -29,6 +30,7 @@ import (
        "github.com/apache/dubbo-kubernetes/pkg/config/host"
        route "github.com/kdubbo/xds-api/route/v1"
        matcher "github.com/kdubbo/xds-api/type/matcher/v1"
+       "google.golang.org/protobuf/types/known/durationpb"
        "google.golang.org/protobuf/types/known/wrapperspb"
        sigsk8siogatewayapiapisv1 "sigs.k8s.io/gateway-api/apis/v1"
 )
@@ -413,14 +415,21 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes 
[]config.Config, hostName host.N
                        // Build route match from HTTPRoute matches
                        routeMatch := 
buildRouteMatchFromHTTPRouteMatches(rule.Matches)
 
+                       routeAction := &route.RouteAction{
+                               ClusterSpecifier: 
&route.RouteAction_WeightedClusters{
+                                       WeightedClusters: weightedClusters,
+                               },
+                       }
+                       if rule.Timeouts != nil {
+                               if timeout := 
gatewayAPIDurationToProto(rule.Timeouts.Request); timeout != nil {
+                                       routeAction.Timeout = timeout
+                               }
+                       }
+
                        builtRoute := &route.Route{
                                Match: routeMatch,
                                Action: &route.Route_Route{
-                                       Route: &route.RouteAction{
-                                               ClusterSpecifier: 
&route.RouteAction_WeightedClusters{
-                                                       WeightedClusters: 
weightedClusters,
-                                               },
-                                       },
+                                       Route: routeAction,
                                },
                        }
 
@@ -433,6 +442,18 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes 
[]config.Config, hostName host.N
        return allRoutes
 }
 
+func gatewayAPIDurationToProto(duration *sigsk8siogatewayapiapisv1.Duration) 
*durationpb.Duration {
+       if duration == nil {
+               return nil
+       }
+       parsed, err := time.ParseDuration(string(*duration))
+       if err != nil {
+               log.Warnf("invalid HTTPRoute timeout duration %q: %v", 
*duration, err)
+               return nil
+       }
+       return durationpb.New(parsed)
+}
+
 // filterHTTPRoutesByGateway filters HTTPRoutes by parentRef to match the 
given Gateway
 func filterHTTPRoutesByGateway(httpRoutes []config.Config, gatewayName, 
gatewayNamespace string, port int) []config.Config {
        if gatewayName == "" {
diff --git a/dubbod/discovery/pkg/networking/grpcgen/rds_test.go 
b/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
index 9573a07f..7b28cc3c 100644
--- a/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
+++ b/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
@@ -18,6 +18,7 @@ package grpcgen
 import (
        "reflect"
        "testing"
+       "time"
 
        "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/config/memory"
        "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
@@ -108,6 +109,32 @@ func 
TestBuildHTTPRouteProxylessOutboundUsesServiceAttachedHTTPRoute(t *testing.
        }
 }
 
+func TestBuildHTTPRouteSetsGatewayAPIRequestTimeout(t *testing.T) {
+       cfg := newServiceAttachedHTTPRouteConfig("reviews-timeout", 
"moviereview", "reviews", 9080)
+       spec := cfg.Spec.(*gatewayv1.HTTPRouteSpec)
+       spec.Rules[0].Timeouts = &gatewayv1.HTTPRouteTimeouts{
+               Request: ptrTo(gatewayv1.Duration("500ms")),
+       }
+       push := newRDSTestPushContext(t, []config.Config{cfg}, []*model.Service{
+               newRDSTestService("reviews", "moviereview", 
"reviews.moviereview.svc.cluster.local", 9080),
+               newRDSTestService("reviews-v1", "moviereview", 
"reviews-v1.moviereview.svc.cluster.local", 9080),
+               newRDSTestService("reviews-v2", "moviereview", 
"reviews-v2.moviereview.svc.cluster.local", 9080),
+       })
+
+       rc := buildHTTPRoute(
+               &model.Proxy{ID: "moviepage.moviereview", Type: 
model.Proxyless},
+               push,
+               "outbound|9080||reviews.moviereview.svc.cluster.local",
+       )
+       if rc == nil {
+               t.Fatal("buildHTTPRoute() returned nil")
+       }
+       timeout := rc.VirtualHosts[0].Routes[0].GetRoute().GetTimeout()
+       if timeout == nil || timeout.AsDuration() != 500*time.Millisecond {
+               t.Fatalf("timeout = %v, want 500ms", timeout)
+       }
+}
+
 func newRDSTestPushContext(t *testing.T, configs []config.Config, services 
[]*model.Service) *model.PushContext {
        t.Helper()
 
diff --git a/go.mod b/go.mod
index 14e0205f..842f6187 100644
--- a/go.mod
+++ b/go.mod
@@ -55,7 +55,7 @@ require (
        github.com/heroku/color v0.0.6
        github.com/kdubbo/api v0.0.0-20260514125132-e21515234c56
        github.com/kdubbo/client-go v0.0.0-20260514125254-aa566bd2f6d5
-       github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f
+       github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162
        github.com/moby/term v0.5.2
        github.com/ory/viper v1.7.5
        github.com/pkg/errors v0.9.1
diff --git a/go.sum b/go.sum
index 52a98f00..c2e32c9c 100644
--- a/go.sum
+++ b/go.sum
@@ -339,10 +339,8 @@ github.com/kdubbo/api v0.0.0-20260514125132-e21515234c56 
h1:yNpHQ+NqBCFVHG4mGJ9Q
 github.com/kdubbo/api v0.0.0-20260514125132-e21515234c56/go.mod 
h1:8BtJiIovg7QCPsCxXcw3gDf922VcvYq5ihOSvj49Rq8=
 github.com/kdubbo/client-go v0.0.0-20260514125254-aa566bd2f6d5 
h1:ynMV7FtdzoEMBGPy8xvljPXt8fWTzFerZ2ZbSXS4erE=
 github.com/kdubbo/client-go v0.0.0-20260514125254-aa566bd2f6d5/go.mod 
h1:NYuRXnoNWCR5uZLjLmWi1ACz+GYFHjckMtLJHWRI4VU=
-github.com/kdubbo/xds-api v0.0.0-20260618170142-573d9453aff2 
h1:hOAD6w8dNGZL/C2LyWVgLQ/CZ3CqatWlyEb49nRcPWE=
-github.com/kdubbo/xds-api v0.0.0-20260618170142-573d9453aff2/go.mod 
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
-github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f 
h1:uO3+UyFwH5nyycY8VxHMt0tL4029SNrVknSxkyNf47g=
-github.com/kdubbo/xds-api v0.0.0-20260627135022-d265dbcbd12f/go.mod 
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
+github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162 
h1:8T6FM/7KYiX84f8wMNmWxZ4YMv+uONCkZqTKiM6m/M8=
+github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162/go.mod 
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
 github.com/kevinburke/ssh_config v1.2.0 
h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod 
h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.5.0/go.mod 
h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=


Reply via email to