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

AlexStocks 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 d4733b12b feat(triple): share HTTP/3 QUIC config with client (#3620)
d4733b12b is described below

commit d4733b12bf965e4be1516de5164833ec007aa7c3
Author: CAICAII <[email protected]>
AuthorDate: Fri Aug 14 12:27:06 2026 +0800

    feat(triple): share HTTP/3 QUIC config with client (#3620)
    
    * feat(triple): share http3 quic config with client
    
    Signed-off-by: CAICAIIs <[email protected]>
---
 protocol/triple/client.go                          |  34 ++++--
 protocol/triple/client_external_test.go            |  30 +++++
 protocol/triple/client_test.go                     |  29 ++++-
 protocol/triple/dual_transport.go                  |  24 +++-
 .../http3config}/http3_config.go                   |   9 +-
 .../internal/http3config/http3_config_test.go      | 134 +++++++++++++++++++++
 protocol/triple/triple_protocol/server.go          |   5 +-
 protocol/triple/triple_protocol/server_test.go     |  46 -------
 8 files changed, 243 insertions(+), 68 deletions(-)

diff --git a/protocol/triple/client.go b/protocol/triple/client.go
index 382b3fa79..6aa949609 100644
--- a/protocol/triple/client.go
+++ b/protocol/triple/client.go
@@ -45,6 +45,7 @@ import (
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/global"
+       "dubbo.apache.org/dubbo-go/v3/protocol/triple/internal/http3config"
        tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
        dubbotls "dubbo.apache.org/dubbo-go/v3/tls"
 )
@@ -226,27 +227,40 @@ func newClientManager(url *common.URL) (*clientManager, 
error) {
                        return nil, fmt.Errorf("TRIPLE http3 client must have 
TLS config, but TLS config is nil")
                }
 
-               // HTTP/3 transport maps keepalive to quic-go's KeepAlivePeriod 
and MaxIdleTimeout;
-               // All other QUIC knobs keep the quic.Config defaults.
+               var http3Config *global.Http3Config
+               if tripleConf != nil {
+                       http3Config = tripleConf.Http3
+               }
+               quicConfig, configErr := http3config.NewQUICConfig(http3Config, 
&quic.Config{
+                       KeepAlivePeriod: keepAliveInterval,
+                       MaxIdleTimeout:  keepAliveTimeout,
+               })
+               if configErr != nil {
+                       return nil, configErr
+               }
+
                transport = &http3.Transport{
                        TLSClientConfig: cfg,
-                       QUICConfig: &quic.Config{
-                               // ref: 
https://quic-go.net/docs/quic/connection/#keeping-a-connection-alive
-                               KeepAlivePeriod: keepAliveInterval,
-                               // ref: 
https://quic-go.net/docs/quic/connection/#idle-timeout
-                               MaxIdleTimeout: keepAliveTimeout,
-                       },
+                       QUICConfig:      quicConfig,
                }
-
                logger.Info("[Triple][Client] triple http3 client transport 
init successfully")
        case constant.CallHTTP2AndHTTP3:
                if !tlsFlag {
                        return nil, fmt.Errorf("TRIPLE HTTP/2 and HTTP/3 client 
must have TLS config, but TLS config is nil")
                }
 
+               var http3Config *global.Http3Config
+               if tripleConf != nil {
+                       http3Config = tripleConf.Http3
+               }
+
                // Dual transport lets the client negotiate HTTP/2 or HTTP/3 
with the
                // same URL and keepalive settings.
-               transport = newDualTransport(cfg, keepAliveInterval, 
keepAliveTimeout)
+               dualTransport, configErr := newDualTransport(cfg, http3Config, 
keepAliveInterval, keepAliveTimeout)
+               if configErr != nil {
+                       return nil, configErr
+               }
+               transport = dualTransport
                logger.Info("[Triple][Client] triple HTTP/2 and HTTP/3 client 
transport init successfully")
        default:
                return nil, fmt.Errorf("unsupported http protocol: %s", 
callProtocol)
diff --git a/protocol/triple/client_external_test.go 
b/protocol/triple/client_external_test.go
index bfbd4d641..1a8d01898 100644
--- a/protocol/triple/client_external_test.go
+++ b/protocol/triple/client_external_test.go
@@ -132,6 +132,36 @@ func writePEMFile(filename, blockType string, data []byte) 
error {
        })
 }
 
+func TestReferRejectsInvalidHTTP3ConfigOnDualTransportPath(t *testing.T) {
+       caCertFile, serverCertFile, serverKeyFile, cleanup := 
generateTestCerts(t)
+       defer cleanup()
+
+       proto := triple.GetProtocol()
+       url, err := common.NewURL(
+               "tri://localhost:20000",
+               common.WithMethods([]string{"test"}),
+               common.WithProtocol(triple.TRIPLE),
+               common.WithParamsValue(constant.IDLMode, constant.NONIDL),
+               common.WithParamsValue(constant.SerializationKey, 
constant.MsgpackSerialization),
+       )
+       require.NoError(t, err)
+       url.SetAttribute(constant.TLSConfigKey, &global.TLSConfig{
+               CACertFile:  caCertFile,
+               TLSCertFile: serverCertFile,
+               TLSKeyFile:  serverKeyFile,
+       })
+       url.SetAttribute(constant.TripleConfigKey, &global.TripleConfig{
+               Http3: &global.Http3Config{
+                       Enable:          true,
+                       KeepAlivePeriod: "invalid",
+               },
+       })
+       url.SetParam(constant.SslEnabledKey, "true")
+
+       invoker := proto.Refer(url)
+       require.Nil(t, invoker)
+}
+
 // testClientInvokeWithTimeout tests that a Triple client invocation correctly 
times out.
 func testClientInvokeWithTimeout(t *testing.T, tlsConfig *global.TLSConfig) {
 
diff --git a/protocol/triple/client_test.go b/protocol/triple/client_test.go
index bebf97518..01831a79e 100644
--- a/protocol/triple/client_test.go
+++ b/protocol/triple/client_test.go
@@ -26,6 +26,8 @@ import (
 )
 
 import (
+       "github.com/quic-go/quic-go/http3"
+
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 
@@ -86,7 +88,8 @@ func TestDualTransport(t *testing.T) {
        keepAliveTimeout := 5 * time.Second
 
        // Test newDualTransport function
-       transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+       transport, err := newDualTransport(nil, nil, keepAliveInterval, 
keepAliveTimeout)
+       require.NoError(t, err)
        assert.NotNil(t, transport)
 
        // Verify that transport implements http.RoundTripper interface
@@ -479,7 +482,8 @@ func TestDualTransport_Structure(t *testing.T) {
        keepAliveInterval := 30 * time.Second
        keepAliveTimeout := 5 * time.Second
 
-       transport := newDualTransport(nil, keepAliveInterval, keepAliveTimeout)
+       transport, err := newDualTransport(nil, nil, keepAliveInterval, 
keepAliveTimeout)
+       require.NoError(t, err)
        assert.NotNil(t, transport)
 
        dt, ok := transport.(*dualTransport)
@@ -489,6 +493,27 @@ func TestDualTransport_Structure(t *testing.T) {
        assert.NotNil(t, dt.altSvcCache)
 }
 
+func TestDualTransport_HTTP3ConfigOverridesKeepAliveDefaults(t *testing.T) {
+       transport, err := newDualTransport(nil, &global.Http3Config{
+               KeepAlivePeriod:       "45s",
+               MaxIdleTimeout:        "6s",
+               MaxIncomingStreams:    11,
+               MaxIncomingUniStreams: 12,
+       }, 30*time.Second, 5*time.Second)
+       require.NoError(t, err)
+
+       dt, ok := transport.(*dualTransport)
+       require.True(t, ok)
+
+       http3Transport, ok := dt.http3Transport.(*http3.Transport)
+       require.True(t, ok)
+       require.NotNil(t, http3Transport.QUICConfig)
+       assert.Equal(t, 45*time.Second, 
http3Transport.QUICConfig.KeepAlivePeriod)
+       assert.Equal(t, 6*time.Second, http3Transport.QUICConfig.MaxIdleTimeout)
+       assert.Equal(t, int64(11), http3Transport.QUICConfig.MaxIncomingStreams)
+       assert.Equal(t, int64(12), 
http3Transport.QUICConfig.MaxIncomingUniStreams)
+}
+
 func Test_newClientManager_HTTP2WithTLS(t *testing.T) {
        // This test requires valid TLS config files
        // Skip if files don't exist
diff --git a/protocol/triple/dual_transport.go 
b/protocol/triple/dual_transport.go
index f963d158f..583242183 100644
--- a/protocol/triple/dual_transport.go
+++ b/protocol/triple/dual_transport.go
@@ -39,6 +39,8 @@ import (
 
 import (
        "dubbo.apache.org/dubbo-go/v3/common/constant"
+       "dubbo.apache.org/dubbo-go/v3/global"
+       "dubbo.apache.org/dubbo-go/v3/protocol/triple/internal/http3config"
        tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
 )
 
@@ -89,19 +91,29 @@ type dualTransport struct {
 }
 
 // newDualTransport creates a new dual transport that supports both HTTP/2 and 
HTTP/3
-func newDualTransport(tlsConfig *tls.Config, keepAliveInterval, 
keepAliveTimeout time.Duration) http.RoundTripper {
+func newDualTransport(
+       tlsConfig *tls.Config,
+       http3Config *global.Http3Config,
+       keepAliveInterval,
+       keepAliveTimeout time.Duration,
+) (http.RoundTripper, error) {
        http2Transport := &http2.Transport{
                TLSClientConfig: tlsConfig,
                ReadIdleTimeout: keepAliveInterval,
                PingTimeout:     keepAliveTimeout,
        }
 
+       quicConfig, err := http3config.NewQUICConfig(http3Config, &quic.Config{
+               KeepAlivePeriod: keepAliveInterval,
+               MaxIdleTimeout:  keepAliveTimeout,
+       })
+       if err != nil {
+               return nil, err
+       }
+
        http3Transport := &http3.Transport{
                TLSClientConfig: tlsConfig,
-               QUICConfig: &quic.Config{
-                       KeepAlivePeriod: keepAliveInterval,
-                       MaxIdleTimeout:  keepAliveTimeout,
-               },
+               QUICConfig:      quicConfig,
        }
 
        return &dualTransport{
@@ -111,7 +123,7 @@ func newDualTransport(tlsConfig *tls.Config, 
keepAliveInterval, keepAliveTimeout
                probeTimeout:   defaultH3ProbeTimeout,
                baseCooldown:   defaultH3BaseCooldown,
                maxCooldown:    defaultH3MaxCooldown,
-       }
+       }, nil
 }
 
 // RoundTrip implements http.RoundTripper interface with HTTP Alternative 
Services support
diff --git a/protocol/triple/triple_protocol/http3_config.go 
b/protocol/triple/internal/http3config/http3_config.go
similarity index 86%
rename from protocol/triple/triple_protocol/http3_config.go
rename to protocol/triple/internal/http3config/http3_config.go
index 93a520ad8..89bf86964 100644
--- a/protocol/triple/triple_protocol/http3_config.go
+++ b/protocol/triple/internal/http3config/http3_config.go
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package triple_protocol
+package http3config
 
 import (
        "fmt"
@@ -30,8 +30,13 @@ import (
        "dubbo.apache.org/dubbo-go/v3/global"
 )
 
-func newQUICConfig(http3Config *global.Http3Config) (*quic.Config, error) {
+// NewQUICConfig maps HTTP/3 transport configuration over optional default 
QUIC settings.
+func NewQUICConfig(http3Config *global.Http3Config, defaults *quic.Config) 
(*quic.Config, error) {
        quicConfig := &quic.Config{}
+       if defaults != nil {
+               quicConfigCopy := *defaults
+               quicConfig = &quicConfigCopy
+       }
        if http3Config == nil {
                return quicConfig, nil
        }
diff --git a/protocol/triple/internal/http3config/http3_config_test.go 
b/protocol/triple/internal/http3config/http3_config_test.go
new file mode 100644
index 000000000..2d3982d8c
--- /dev/null
+++ b/protocol/triple/internal/http3config/http3_config_test.go
@@ -0,0 +1,134 @@
+/*
+ * 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 http3config
+
+import (
+       "testing"
+       "time"
+)
+
+import (
+       "github.com/quic-go/quic-go"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+import (
+       "dubbo.apache.org/dubbo-go/v3/global"
+)
+
+func TestNewQUICConfig(t *testing.T) {
+       t.Run("defaults_preserved_when_unset", func(t *testing.T) {
+               quicConfig, err := NewQUICConfig(&global.Http3Config{}, nil)
+               require.NoError(t, err)
+               require.NotNil(t, quicConfig)
+               assert.Zero(t, quicConfig.KeepAlivePeriod)
+               assert.Zero(t, quicConfig.MaxIdleTimeout)
+               assert.Zero(t, quicConfig.MaxIncomingStreams)
+               assert.Zero(t, quicConfig.MaxIncomingUniStreams)
+       })
+
+       t.Run("explicit_fields_are_mapped", func(t *testing.T) {
+               quicConfig, err := NewQUICConfig(&global.Http3Config{
+                       KeepAlivePeriod:       "15s",
+                       MaxIdleTimeout:        "30s",
+                       MaxIncomingStreams:    128,
+                       MaxIncomingUniStreams: 64,
+               }, nil)
+               require.NoError(t, err)
+               require.NotNil(t, quicConfig)
+               assert.Equal(t, 15*time.Second, quicConfig.KeepAlivePeriod)
+               assert.Equal(t, 30*time.Second, quicConfig.MaxIdleTimeout)
+               assert.Equal(t, int64(128), quicConfig.MaxIncomingStreams)
+               assert.Equal(t, int64(64), quicConfig.MaxIncomingUniStreams)
+       })
+
+       t.Run("invalid_keep_alive_period_returns_error", func(t *testing.T) {
+               quicConfig, err := NewQUICConfig(&global.Http3Config{
+                       KeepAlivePeriod: "invalid",
+               }, nil)
+               require.Error(t, err)
+               assert.Nil(t, quicConfig)
+               assert.ErrorContains(t, err, "keep-alive-period")
+       })
+
+       t.Run("invalid_max_idle_timeout_returns_error", func(t *testing.T) {
+               quicConfig, err := NewQUICConfig(&global.Http3Config{
+                       MaxIdleTimeout: "invalid",
+               }, nil)
+               require.Error(t, err)
+               assert.Nil(t, quicConfig)
+               assert.ErrorContains(t, err, "max-idle-timeout")
+       })
+       t.Run("nil_config_uses_defaults", func(t *testing.T) {
+               defaults := &quic.Config{
+                       KeepAlivePeriod: 10 * time.Second,
+                       MaxIdleTimeout:  20 * time.Second,
+               }
+
+               quicConfig, err := NewQUICConfig(nil, defaults)
+               require.NoError(t, err)
+               require.NotNil(t, quicConfig)
+               assert.NotSame(t, defaults, quicConfig)
+               assert.Equal(t, 10*time.Second, quicConfig.KeepAlivePeriod)
+               assert.Equal(t, 20*time.Second, quicConfig.MaxIdleTimeout)
+       })
+
+       t.Run("defaults_are_used_when_fields_unset", func(t *testing.T) {
+               defaults := &quic.Config{
+                       KeepAlivePeriod: 10 * time.Second,
+                       MaxIdleTimeout:  20 * time.Second,
+               }
+
+               quicConfig, err := NewQUICConfig(&global.Http3Config{}, 
defaults)
+               require.NoError(t, err)
+               require.NotNil(t, quicConfig)
+               assert.NotSame(t, defaults, quicConfig)
+               assert.Equal(t, 10*time.Second, quicConfig.KeepAlivePeriod)
+               assert.Equal(t, 20*time.Second, quicConfig.MaxIdleTimeout)
+       })
+
+       t.Run("explicit_fields_override_defaults", func(t *testing.T) {
+               quicConfig, err := NewQUICConfig(&global.Http3Config{
+                       KeepAlivePeriod: "15s",
+                       MaxIdleTimeout:  "30s",
+               }, &quic.Config{
+                       KeepAlivePeriod: 10 * time.Second,
+                       MaxIdleTimeout:  20 * time.Second,
+               })
+               require.NoError(t, err)
+               require.NotNil(t, quicConfig)
+               assert.Equal(t, 15*time.Second, quicConfig.KeepAlivePeriod)
+               assert.Equal(t, 30*time.Second, quicConfig.MaxIdleTimeout)
+       })
+
+       t.Run("explicit_zero_duration_overrides_default", func(t *testing.T) {
+               quicConfig, err := NewQUICConfig(&global.Http3Config{
+                       KeepAlivePeriod: "0s",
+                       MaxIdleTimeout:  "0s",
+               }, &quic.Config{
+                       KeepAlivePeriod: 10 * time.Second,
+                       MaxIdleTimeout:  20 * time.Second,
+               })
+               require.NoError(t, err)
+               require.NotNil(t, quicConfig)
+               assert.Zero(t, quicConfig.KeepAlivePeriod)
+               assert.Zero(t, quicConfig.MaxIdleTimeout)
+       })
+}
diff --git a/protocol/triple/triple_protocol/server.go 
b/protocol/triple/triple_protocol/server.go
index 409b4e485..10cde267a 100644
--- a/protocol/triple/triple_protocol/server.go
+++ b/protocol/triple/triple_protocol/server.go
@@ -43,6 +43,7 @@ import (
        "dubbo.apache.org/dubbo-go/v3/common"
        "dubbo.apache.org/dubbo-go/v3/common/constant"
        "dubbo.apache.org/dubbo-go/v3/global"
+       "dubbo.apache.org/dubbo-go/v3/protocol/triple/internal/http3config"
        "dubbo.apache.org/dubbo-go/v3/protocol/triple/openapi"
 )
 
@@ -228,7 +229,7 @@ func (s *Server) startHttp3(tlsConf *tls.Config) error {
                http3Config = s.tripleConfig.Http3
        }
 
-       quicConfig, err := newQUICConfig(http3Config)
+       quicConfig, err := http3config.NewQUICConfig(http3Config, nil)
        if err != nil {
                return err
        }
@@ -259,7 +260,7 @@ func (s *Server) startHttp2AndHttp3(tlsConf *tls.Config) 
error {
                http3Config = s.tripleConfig.Http3
        }
 
-       quicConfig, err := newQUICConfig(http3Config)
+       quicConfig, err := http3config.NewQUICConfig(http3Config, nil)
        if err != nil {
                return err
        }
diff --git a/protocol/triple/triple_protocol/server_test.go 
b/protocol/triple/triple_protocol/server_test.go
index f002682b0..c9ef72dd5 100644
--- a/protocol/triple/triple_protocol/server_test.go
+++ b/protocol/triple/triple_protocol/server_test.go
@@ -23,7 +23,6 @@ import (
        "net/http/httptest"
        "net/url"
        "testing"
-       "time"
 )
 
 import (
@@ -103,51 +102,6 @@ func TestServer_RegisterMuxHandle(t *testing.T) {
        }
 }
 
-func TestNewQUICConfig(t *testing.T) {
-       t.Run("defaults_preserved_when_unset", func(t *testing.T) {
-               quicConfig, err := newQUICConfig(&global.Http3Config{})
-               require.NoError(t, err)
-               require.NotNil(t, quicConfig)
-               assert.Zero(t, quicConfig.KeepAlivePeriod)
-               assert.Zero(t, quicConfig.MaxIdleTimeout)
-               assert.Zero(t, quicConfig.MaxIncomingStreams)
-               assert.Zero(t, quicConfig.MaxIncomingUniStreams)
-       })
-
-       t.Run("explicit_fields_are_mapped", func(t *testing.T) {
-               quicConfig, err := newQUICConfig(&global.Http3Config{
-                       KeepAlivePeriod:       "15s",
-                       MaxIdleTimeout:        "30s",
-                       MaxIncomingStreams:    128,
-                       MaxIncomingUniStreams: 64,
-               })
-               require.NoError(t, err)
-               require.NotNil(t, quicConfig)
-               assert.Equal(t, 15*time.Second, quicConfig.KeepAlivePeriod)
-               assert.Equal(t, 30*time.Second, quicConfig.MaxIdleTimeout)
-               assert.Equal(t, int64(128), quicConfig.MaxIncomingStreams)
-               assert.Equal(t, int64(64), quicConfig.MaxIncomingUniStreams)
-       })
-
-       t.Run("invalid_keep_alive_period_returns_error", func(t *testing.T) {
-               quicConfig, err := newQUICConfig(&global.Http3Config{
-                       KeepAlivePeriod: "invalid",
-               })
-               require.Error(t, err)
-               assert.Nil(t, quicConfig)
-               assert.ErrorContains(t, err, "keep-alive-period")
-       })
-
-       t.Run("invalid_max_idle_timeout_returns_error", func(t *testing.T) {
-               quicConfig, err := newQUICConfig(&global.Http3Config{
-                       MaxIdleTimeout: "invalid",
-               })
-               require.Error(t, err)
-               assert.Nil(t, quicConfig)
-               assert.ErrorContains(t, err, "max-idle-timeout")
-       })
-}
-
 func TestServer_HTTP3PathsUseQUICConfigHelper(t *testing.T) {
        t.Run("start_http3_returns_parse_error", func(t *testing.T) {
                srv := NewServer("127.0.0.1:0", &global.TripleConfig{

Reply via email to