You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by xu...@apache.org on 2026/08/04 23:50:26 UTC

(dubbo-go) branch develop updated: feat(triple): add server http3 quic config helper (#3299)

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 c7452e174 feat(triple): add server http3 quic config helper (#3299)
c7452e174 is described below

commit c7452e1749f9f558ccf9665db0a32bfd83113ce0
Author: CAICAII <33...@qq.com>
AuthorDate: Wed Aug 5 07:50:20 2026 +0800

    feat(triple): add server http3 quic config helper (#3299)
    
    * feat(triple): add server http3 quic config helper
    
    * refactor(triple): drop http3 config compat mirror
    
    * test(triple): satisfy lint for http3 server tests
    
    * fix(global): align http3 json tags and decode aliases
    
    * style(global): format http3 config file
    
    * refactor(global): remove http3 json compat decoder
    
    Signed-off-by: CAICAIIs <33...@qq.com>
    
    ---------
    
    Signed-off-by: CAICAIIs <33...@qq.com>
---
 global/config_test.go                           | 57 +++++++++++++++++-
 global/http3_config.go                          | 28 +++++++--
 protocol/triple/triple_protocol/http3_config.go | 64 ++++++++++++++++++++
 protocol/triple/triple_protocol/server.go       | 28 +++++++--
 protocol/triple/triple_protocol/server_test.go  | 78 ++++++++++++++++++++++++-
 5 files changed, 242 insertions(+), 13 deletions(-)

diff --git a/global/config_test.go b/global/config_test.go
index 8efe71b7d..e5e948f32 100644
--- a/global/config_test.go
+++ b/global/config_test.go
@@ -18,6 +18,7 @@
 package global
 
 import (
+	"encoding/json"
 	"reflect"
 	"testing"
 )
@@ -1165,13 +1166,22 @@ func TestDefaultTripleConfig(t *testing.T) {
 func TestHttp3ConfigClone(t *testing.T) {
 	t.Run("clone_http3_config", func(t *testing.T) {
 		http3 := &Http3Config{
-			Enable:      true,
-			Negotiation: false,
+			Enable:                true,
+			Negotiation:           false,
+			KeepAlivePeriod:       "15s",
+			MaxIdleTimeout:        "30s",
+			MaxIncomingStreams:    128,
+			MaxIncomingUniStreams: 64,
 		}
 		cloned := http3.Clone()
 		assert.NotNil(t, cloned)
+		assert.NotSame(t, http3, cloned)
 		assert.Equal(t, http3.Enable, cloned.Enable)
 		assert.Equal(t, http3.Negotiation, cloned.Negotiation)
+		assert.Equal(t, http3.KeepAlivePeriod, cloned.KeepAlivePeriod)
+		assert.Equal(t, http3.MaxIdleTimeout, cloned.MaxIdleTimeout)
+		assert.Equal(t, http3.MaxIncomingStreams, cloned.MaxIncomingStreams)
+		assert.Equal(t, http3.MaxIncomingUniStreams, cloned.MaxIncomingUniStreams)
 	})
 
 	t.Run("clone_nil_http3_config", func(t *testing.T) {
@@ -1184,8 +1194,13 @@ func TestHttp3ConfigClone(t *testing.T) {
 		http3 := DefaultHttp3Config()
 		cloned := http3.Clone()
 		assert.NotNil(t, cloned)
+		assert.NotSame(t, http3, cloned)
 		assert.Equal(t, http3.Enable, cloned.Enable)
 		assert.Equal(t, http3.Negotiation, cloned.Negotiation)
+		assert.Equal(t, http3.KeepAlivePeriod, cloned.KeepAlivePeriod)
+		assert.Equal(t, http3.MaxIdleTimeout, cloned.MaxIdleTimeout)
+		assert.Equal(t, http3.MaxIncomingStreams, cloned.MaxIncomingStreams)
+		assert.Equal(t, http3.MaxIncomingUniStreams, cloned.MaxIncomingUniStreams)
 	})
 }
 
@@ -1194,8 +1209,46 @@ func TestDefaultHttp3Config(t *testing.T) {
 	t.Run("default_http3_config", func(t *testing.T) {
 		http3 := DefaultHttp3Config()
 		assert.NotNil(t, http3)
+		assert.False(t, http3.Enable)
+		assert.True(t, http3.Negotiation)
+		assert.Empty(t, http3.KeepAlivePeriod)
+		assert.Empty(t, http3.MaxIdleTimeout)
+		assert.Zero(t, http3.MaxIncomingStreams)
+		assert.Zero(t, http3.MaxIncomingUniStreams)
 	})
 }
+
+func TestHttp3ConfigJSONTags(t *testing.T) {
+	http3 := &Http3Config{
+		Enable:                true,
+		Negotiation:           true,
+		KeepAlivePeriod:       "15s",
+		MaxIdleTimeout:        "30s",
+		MaxIncomingStreams:    128,
+		MaxIncomingUniStreams: 64,
+	}
+
+	data, err := json.Marshal(http3)
+	require.NoError(t, err)
+	assert.Contains(t, string(data), "\"keep-alive-period\":\"15s\"")
+	assert.Contains(t, string(data), "\"max-idle-timeout\":\"30s\"")
+	assert.Contains(t, string(data), "\"max-incoming-streams\":128")
+	assert.Contains(t, string(data), "\"max-incoming-uni-streams\":64")
+
+	var decoded Http3Config
+	err = json.Unmarshal([]byte(`{
+		"enable": true,
+		"negotiation": true,
+		"keep-alive-period": "15s",
+		"max-idle-timeout": "30s",
+		"max-incoming-streams": 128,
+		"max-incoming-uni-streams": 64
+	}`), &decoded)
+	require.NoError(t, err)
+	assert.Equal(t, http3, &decoded)
+
+}
+
 func TestConsumerConfigClone(t *testing.T) {
 	t.Run("clone_full_consumer_config", func(t *testing.T) {
 		consumer := &ConsumerConfig{
diff --git a/global/http3_config.go b/global/http3_config.go
index c079afa52..0bd4d210d 100644
--- a/global/http3_config.go
+++ b/global/http3_config.go
@@ -34,14 +34,28 @@ type Http3Config struct {
 	// ref: https://quic-go.net/docs/http3/server/#advertising-http3-via-alt-svc
 	Negotiation bool `yaml:"negotiation" json:"negotiation,omitempty"`
 
-	// TODO: add more params about http3
+	// KeepAlivePeriod defines how often to send keep-alive packets.
+	KeepAlivePeriod string `yaml:"keep-alive-period" json:"keep-alive-period,omitempty"`
+
+	// MaxIdleTimeout defines the maximum idle timeout for QUIC connections.
+	MaxIdleTimeout string `yaml:"max-idle-timeout" json:"max-idle-timeout,omitempty"`
+
+	// MaxIncomingStreams defines the maximum number of concurrent bidirectional streams.
+	MaxIncomingStreams int64 `yaml:"max-incoming-streams" json:"max-incoming-streams,omitempty"`
+
+	// MaxIncomingUniStreams defines the maximum number of concurrent unidirectional streams.
+	MaxIncomingUniStreams int64 `yaml:"max-incoming-uni-streams" json:"max-incoming-uni-streams,omitempty"`
 }
 
 // DefaultHttp3Config returns a default Http3Config instance.
 func DefaultHttp3Config() *Http3Config {
 	return &Http3Config{
-		Enable:      false,
-		Negotiation: true,
+		Enable:                false,
+		Negotiation:           true,
+		KeepAlivePeriod:       "",
+		MaxIdleTimeout:        "",
+		MaxIncomingStreams:    0,
+		MaxIncomingUniStreams: 0,
 	}
 }
 
@@ -52,7 +66,11 @@ func (t *Http3Config) Clone() *Http3Config {
 	}
 
 	return &Http3Config{
-		Enable:      t.Enable,
-		Negotiation: t.Negotiation,
+		Enable:                t.Enable,
+		Negotiation:           t.Negotiation,
+		KeepAlivePeriod:       t.KeepAlivePeriod,
+		MaxIdleTimeout:        t.MaxIdleTimeout,
+		MaxIncomingStreams:    t.MaxIncomingStreams,
+		MaxIncomingUniStreams: t.MaxIncomingUniStreams,
 	}
 }
diff --git a/protocol/triple/triple_protocol/http3_config.go b/protocol/triple/triple_protocol/http3_config.go
new file mode 100644
index 000000000..93a520ad8
--- /dev/null
+++ b/protocol/triple/triple_protocol/http3_config.go
@@ -0,0 +1,64 @@
+/*
+ * 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 triple_protocol
+
+import (
+	"fmt"
+	"time"
+)
+
+import (
+	"github.com/quic-go/quic-go"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/global"
+)
+
+func newQUICConfig(http3Config *global.Http3Config) (*quic.Config, error) {
+	quicConfig := &quic.Config{}
+	if http3Config == nil {
+		return quicConfig, nil
+	}
+
+	if http3Config.KeepAlivePeriod != "" {
+		keepAlivePeriod, err := time.ParseDuration(http3Config.KeepAlivePeriod)
+		if err != nil {
+			return nil, fmt.Errorf("invalid http3 keep-alive-period %q: %w", http3Config.KeepAlivePeriod, err)
+		}
+		quicConfig.KeepAlivePeriod = keepAlivePeriod
+	}
+
+	if http3Config.MaxIdleTimeout != "" {
+		maxIdleTimeout, err := time.ParseDuration(http3Config.MaxIdleTimeout)
+		if err != nil {
+			return nil, fmt.Errorf("invalid http3 max-idle-timeout %q: %w", http3Config.MaxIdleTimeout, err)
+		}
+		quicConfig.MaxIdleTimeout = maxIdleTimeout
+	}
+
+	// Preserve quic-go defaults when these fields are left unset in config.
+	if http3Config.MaxIncomingStreams != 0 {
+		quicConfig.MaxIncomingStreams = http3Config.MaxIncomingStreams
+	}
+	if http3Config.MaxIncomingUniStreams != 0 {
+		quicConfig.MaxIncomingUniStreams = http3Config.MaxIncomingUniStreams
+	}
+
+	return quicConfig, nil
+}
diff --git a/protocol/triple/triple_protocol/server.go b/protocol/triple/triple_protocol/server.go
index 6402b9c7f..ea88764a4 100644
--- a/protocol/triple/triple_protocol/server.go
+++ b/protocol/triple/triple_protocol/server.go
@@ -29,7 +29,6 @@ import (
 
 	"github.com/dubbogo/grpc-go"
 
-	"github.com/quic-go/quic-go"
 	"github.com/quic-go/quic-go/http3"
 
 	"golang.org/x/net/http2"
@@ -221,15 +220,24 @@ func (s *Server) startHttp3(tlsConf *tls.Config) error {
 		return fmt.Errorf("TRIPLE HTTP/3 Server must have TLS config, but TLS config is nil")
 	}
 
+	var http3Config *global.Http3Config
+	if s.tripleConfig != nil {
+		http3Config = s.tripleConfig.Http3
+	}
+
+	quicConfig, err := newQUICConfig(http3Config)
+	if err != nil {
+		return err
+	}
+
 	s.http3Srv = &http3.Server{
 		Addr:    s.addr,
 		Handler: s.mux,
 		// Adapt and enhance a generic tls.Config object into a configuration
 		// specifically for HTTP/3 services.
 		// ref: https://quic-go.net/docs/http3/server/#setting-up-a-http3server
-		TLSConfig: http3.ConfigureTLSConfig(tlsConf),
-		// TODO: Detailed QUIC configuration.
-		QUICConfig: &quic.Config{},
+		TLSConfig:  http3.ConfigureTLSConfig(tlsConf),
+		QUICConfig: quicConfig,
 	}
 
 	logger.Debugf("[Triple][Server] triple HTTP/3 Server starting on %v", s.addr)
@@ -243,12 +251,22 @@ func (s *Server) startHttp2AndHttp3(tlsConf *tls.Config) error {
 		return fmt.Errorf("TRIPLE HTTP/2 and HTTP/3 Server must have TLS config, but TLS config is nil")
 	}
 
+	var http3Config *global.Http3Config
+	if s.tripleConfig != nil {
+		http3Config = s.tripleConfig.Http3
+	}
+
+	quicConfig, err := newQUICConfig(http3Config)
+	if err != nil {
+		return err
+	}
+
 	// Start HTTP/3 server first to get its configuration
 	s.http3Srv = &http3.Server{
 		Addr:       s.addr,
 		Handler:    s.mux,
 		TLSConfig:  http3.ConfigureTLSConfig(tlsConf),
-		QUICConfig: &quic.Config{},
+		QUICConfig: quicConfig,
 	}
 
 	// Create Alt-Svc handler wrapper for HTTP/2 server
diff --git a/protocol/triple/triple_protocol/server_test.go b/protocol/triple/triple_protocol/server_test.go
index d2693c98d..84d87ae45 100644
--- a/protocol/triple/triple_protocol/server_test.go
+++ b/protocol/triple/triple_protocol/server_test.go
@@ -18,10 +18,12 @@
 package triple_protocol
 
 import (
+	"crypto/tls"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
 	"testing"
+	"time"
 )
 
 import (
@@ -90,7 +92,7 @@ func TestServer_RegisterMuxHandle(t *testing.T) {
 		},
 	})
 	for _, test := range tests {
-		err := srv.RegisterUnaryHandler(test.path, nil, nil)
+		err := test.registerFunc(srv, test.path)
 		require.NoError(t, err)
 		_, pattern := srv.mux.Handler(&http.Request{
 			URL: &url.URL{
@@ -101,6 +103,80 @@ 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{
+			Http3: &global.Http3Config{
+				KeepAlivePeriod: "invalid",
+			},
+		})
+
+		err := srv.startHttp3(&tls.Config{})
+		require.Error(t, err)
+		require.ErrorContains(t, err, "keep-alive-period")
+		assert.Nil(t, srv.http3Srv)
+	})
+
+	t.Run("start_http2_and_http3_returns_parse_error", func(t *testing.T) {
+		srv := NewServer("127.0.0.1:0", &global.TripleConfig{
+			Http3: &global.Http3Config{
+				MaxIdleTimeout: "invalid",
+			},
+		})
+
+		err := srv.startHttp2AndHttp3(&tls.Config{})
+		require.Error(t, err)
+		require.ErrorContains(t, err, "max-idle-timeout")
+		assert.Nil(t, srv.http3Srv)
+		assert.Nil(t, srv.httpSrv)
+	})
+}
+
 func TestServerSetFallbackHTTPHandler(t *testing.T) {
 	srv := NewServer("127.0.0.1:0", nil)
 	srv.SetFallbackHTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {