fix(discov): move etcd hosts from URI authority to path for Go 1.26 compatibility (#5548)

This commit is contained in:
Kevin Wan
2026-04-25 10:48:28 +08:00
committed by GitHub
parent 22bdae0787
commit 4a67261b7b
6 changed files with 120 additions and 6 deletions

View File

@@ -13,10 +13,10 @@ type discovBuilder struct{}
func (b *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) ( func (b *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (
resolver.Resolver, error) { resolver.Resolver, error) {
hosts := strings.FieldsFunc(targets.GetAuthority(target), func(r rune) bool { hosts := strings.FieldsFunc(targets.GetHosts(target), func(r rune) bool {
return r == EndpointSepChar return r == EndpointSepChar
}) })
sub, err := discov.NewSubscriber(hosts, targets.GetEndpoints(target)) sub, err := discov.NewSubscriber(hosts, targets.GetKey(target))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -28,7 +28,7 @@ func TestDiscovBuilder_Build(t *testing.T) {
for _, server := range servers.Servers { for _, server := range servers.Servers {
addrs = append(addrs, server.Address) addrs = append(addrs, server.Address)
} }
u, err := url.Parse(fmt.Sprintf("%s://%s", DiscovScheme, strings.Join(addrs, ","))) u, err := url.Parse(fmt.Sprintf("%s:///%s?key=test", DiscovScheme, strings.Join(addrs, ",")))
assert.NoError(t, err) assert.NoError(t, err)
var b discovBuilder var b discovBuilder

View File

@@ -17,3 +17,29 @@ func GetAuthority(target resolver.Target) string {
func GetEndpoints(target resolver.Target) string { func GetEndpoints(target resolver.Target) string {
return strings.Trim(target.URL.Path, slashSeparator) return strings.Trim(target.URL.Path, slashSeparator)
} }
// GetHosts returns the comma-separated etcd hosts from the target URL.
// It supports two formats:
// - New format (etcd:///h1:port,h2:port?key=k): hosts are in the URL path (empty authority)
// - Legacy format (etcd://h1:port/key): host is in the URL authority
func GetHosts(target resolver.Target) string {
if target.URL.Host == "" {
// New format: hosts encoded in URL path to avoid RFC 3986 authority issues
return GetEndpoints(target)
}
// Legacy format: single host in authority
return target.URL.Host
}
// GetKey returns the etcd key from the target URL.
// It supports two formats:
// - New format (etcd:///h1:port,h2:port?key=k): key is in the "key" query parameter
// - Legacy format (etcd://h1:port/key): key is in the URL path
func GetKey(target resolver.Target) string {
if target.URL.Host == "" {
// New format: key is in the query parameter
return target.URL.Query().Get("key")
}
// Legacy format: key is in the path
return strings.Trim(target.URL.Path, slashSeparator)
}

View File

@@ -87,3 +87,83 @@ func TestGetEndpoints(t *testing.T) {
}) })
} }
} }
func TestGetHosts(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "single host",
url: "etcd:///localhost:2379?key=foo",
want: "localhost:2379",
},
{
name: "multiple hosts",
url: "etcd:///host1:2379,host2:2379,host3:2379?key=foo",
want: "host1:2379,host2:2379,host3:2379",
},
{
name: "legacy single host in authority",
url: "etcd://localhost:2379/my-service",
want: "localhost:2379",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri, err := url.Parse(test.url)
assert.Nil(t, err)
target := resolver.Target{
URL: *uri,
}
assert.Equal(t, test.want, GetHosts(target))
})
}
}
func TestGetKey(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{
name: "simple key",
url: "etcd:///localhost:2379?key=my-service",
want: "my-service",
},
{
name: "key with slashes",
url: "etcd:///localhost:2379?key=%2Fgrpc%2Fmy-service",
want: "/grpc/my-service",
},
{
name: "no key",
url: "etcd:///localhost:2379",
want: "",
},
{
name: "legacy key in path",
url: "etcd://localhost:2379/my-service",
want: "my-service",
},
{
name: "legacy key with leading slash",
url: "etcd://localhost:2379/grpc/my-service",
want: "grpc/my-service",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uri, err := url.Parse(test.url)
assert.Nil(t, err)
target := resolver.Target{
URL: *uri,
}
assert.Equal(t, test.want, GetKey(target))
})
}
}

View File

@@ -2,6 +2,7 @@ package resolver
import ( import (
"fmt" "fmt"
"net/url"
"strings" "strings"
"github.com/zeromicro/go-zero/zrpc/resolver/internal" "github.com/zeromicro/go-zero/zrpc/resolver/internal"
@@ -14,7 +15,9 @@ func BuildDirectTarget(endpoints []string) string {
} }
// BuildDiscovTarget returns a string that represents the given endpoints with discov schema. // BuildDiscovTarget returns a string that represents the given endpoints with discov schema.
// The format is etcd:///host1:port,host2:port?key=<etcd-key> to avoid placing comma-separated
// hosts in the URI authority, which Go 1.26+ rejects per RFC 3986.
func BuildDiscovTarget(endpoints []string, key string) string { func BuildDiscovTarget(endpoints []string, key string) string {
return fmt.Sprintf("%s://%s/%s", internal.EtcdScheme, return fmt.Sprintf("%s:///%s?key=%s", internal.EtcdScheme,
strings.Join(endpoints, internal.EndpointSep), key) strings.Join(endpoints, internal.EndpointSep), url.QueryEscape(key))
} }

View File

@@ -13,5 +13,10 @@ func TestBuildDirectTarget(t *testing.T) {
func TestBuildDiscovTarget(t *testing.T) { func TestBuildDiscovTarget(t *testing.T) {
target := BuildDiscovTarget([]string{"localhost:123", "localhost:456"}, "foo") target := BuildDiscovTarget([]string{"localhost:123", "localhost:456"}, "foo")
assert.Equal(t, "etcd://localhost:123,localhost:456/foo", target) assert.Equal(t, "etcd:///localhost:123,localhost:456?key=foo", target)
}
func TestBuildDiscovTargetWithSlashKey(t *testing.T) {
target := BuildDiscovTarget([]string{"localhost:2379"}, "/grpc/my-service")
assert.Equal(t, "etcd:///localhost:2379?key=%2Fgrpc%2Fmy-service", target)
} }