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 <[email protected]>
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 <[email protected]>
---------
Signed-off-by: CAICAIIs <[email protected]>
---
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) {