vitessio/vitess · error
%s: unsupported URL host %s
Error message
%s: unsupported URL host %s
What it means
The gRPC resolver builder (build) maps the target URL's host to a discovery function; only hosts "vtctld" and "vtgate" are supported. Any other host means the resolver has no way to discover backend addresses, so building the resolver fails with this error. The logPrefix identifies which resolver/cluster failed.
Source
Thrown at go/vt/vtadmin/cluster/resolver/resolver.go:257
return nil, err
}
b.m.Lock()
b.resolvers = append(b.resolvers, r)
b.m.Unlock()
return r, nil
}
func (b *builder) build(target grpcresolver.Target, cc grpcresolver.ClientConn, opts grpcresolver.BuildOptions) (*resolver, error) {
var fn func(context.Context, []string) ([]string, error)
switch target.URL.Host {
case "vtctld":
fn = b.opts.Discovery.DiscoverVtctldAddrs
case "vtgate":
fn = b.opts.Discovery.DiscoverVTGateAddrs
default:
return nil, fmt.Errorf("%s: unsupported URL host %s", logPrefix, target.URL.Host)
}
var sc serviceconfig.Config
if b.opts.BalancerPolicy != "" {
// c.f. https://github.com/grpc/grpc/blob/master/doc/service_config.md#example
scpr := cc.ParseServiceConfig(fmt.Sprintf(`{"loadBalancingConfig": [{ "%s": {} }] }`, b.opts.BalancerPolicy))
if scpr.Err != nil {
return nil, fmt.Errorf("failed to initialize service config with load balancer policy %s: %s", b.opts.BalancerPolicy, scpr.Err)
}
sc = scpr.Config
}
ctx, cancel := context.WithCancel(context.Background())
r := &resolver{
component: target.URL.Host,
// use the original cluster ID (not the sanitized scheme) for debugging/loggingView on GitHub (pinned to 01a25a7d17)
Solutions
- Set the target URL host to exactly 'vtctld' or 'vtgate' (e.g. cluster://mycluster/vtctld).
- Check how the dial target string is constructed/templated for typos.
- Verify scheme parsing: the host is whatever sits between the second '/' and the next '/'.
- If a new component is required, extend build's switch with a discovery function (code change).
Example fix
// before
cc, err := resolver.Build(target, ...) // target: "cluster://vtctlds/account"
// after
cc, err := resolver.Build("cluster://account/vtctld", ...) Defensive patterns
Strategy: validation
Validate before calling
u, err := url.Parse(target)
if err != nil { return err }
if h := u.Host; h != "vtctld" && h != "vtgate" {
return fmt.Errorf("unsupported resolver host %q", h)
} Type guard
func isSupportedResolverTarget(target string) bool {
u, err := url.Parse(target)
return err == nil && (u.Host == "vtctld" || u.Host == "vtgate")
} Try / catch
cc, err := resolver.Build(target, opts)
if err != nil {
if strings.Contains(err.Error(), "unsupported URL host") {
log.Warn("bad dial target, falling back", slog.String("target", target))
return grpc.Dial(fallbackAddr)
}
return err
} Prevention
- Centralize dial-target construction in one helper.
- Use constants for component names instead of interpolated strings.
- Unit-test target templates for every component type.
- Remember the URL host must be the pseudo-host, not a real hostname.
When it happens
Trigger: Calling resolver.Build or mustBuild with a target like "cluster://ks/other" or a mistyped scheme URL whose URL.Host is not exactly 'vtctld' or 'vtgate' — e.g. "cluster://vtctlds/..." (plural) or a full hostname in the target.
Common situations: Constructing gRPC dial targets by hand with wrong pseudo-host names; templated configs where the component segment is interpolated with a typo; adding a new component type without updating the resolver switch.
Related errors
- failed to load static auth plugin. Plugin configured but grp
- failed to parse tablet-filters value %q: %v
- invalid FilterByShard parameter: %v
- duplicate %v/%v entry
- unknown mysqlctl client protocol: %v
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/30af6b923f1eab52.
Report an issue: GitHub.