grpc/grpc-go · error

dns resolver: missing port after port-separator colon

Error message

dns resolver: missing port after port-separator colon

What it means

ErrEndsWithColon is returned by the DNS resolver builder when the target string ends with a colon, implying a host:port separator was started but no port follows. For example 'host:' has a host but no port. Note that '::' (bare IPv6) is valid as a host-only address, but 'host:' or '[::1]:' are not.

Solutions

  1. Include a port after the colon: 'dns:///my-service:8080'.
  2. If no port is intended, omit the colon entirely (the resolver applies a default port if needed).
  3. Validate the target does not end with ':' before dialing.
  4. Fix string formatting that appends ':' without a port value.

Example fix

// before
addr := fmt.Sprintf("%s:", hostname) // produces 'host:'
conn, _ := grpc.Dial("dns:///" + addr)
// after
addr := fmt.Sprintf("%s:%d", hostname, port) // produces 'host:8080'
conn, _ := grpc.Dial("dns:///" + addr)
Defensive patterns

Strategy: validation

Validate before calling

// Validate target does not end with ':' (incomplete port).
func hasValidPort(target string) bool {
    // allow bare IPv6 like '::' but reject 'host:' with no port
    if strings.HasSuffix(target, ":") {
        host := strings.TrimSuffix(target, ":")
        if !strings.Contains(host, ":") { // not IPv6
            return false
        }
    }
    return true
}

Prevention

When it happens

Trigger: Dialing with a target like 'dns:///my-service:' (trailing colon, no port), or an address string that ends with ':' after the resolver parses host and port. The resolver detects the trailing colon as an incomplete host:port pair.

Common situations: String concatenation bug that appends ':' but not a port (e.g., fmt.Sprintf("%s:", host)); config templating that adds a colon delimiter but leaves port empty; user input that omits the port after the colon.

Related errors


AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11). Data as JSON: /api/errors/77a46595a457e6c6. Report an issue: GitHub.

Appendix: source

Thrown at internal/resolver/dns/internal/internal.go:48

// resolver implementation. This allows the default net.Resolver instance to be
// overridden from tests.
type NetResolver interface {
	LookupHost(ctx context.Context, host string) (addrs []string, err error)
	LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error)
	LookupTXT(ctx context.Context, name string) (txts []string, err error)
}

var (
	// ErrMissingAddr is the error returned when building a DNS resolver when
	// the provided target name is empty.
	ErrMissingAddr = errors.New("dns resolver: missing address")

	// ErrEndsWithColon is the error returned when building a DNS resolver when
	// the provided target name ends with a colon that is supposed to be the
	// separator between host and port.  E.g. "::" is a valid address as it is
	// an IPv6 address (host only) and "[::]:" is invalid as it ends with a
	// colon as the host and port separator
	ErrEndsWithColon = errors.New("dns resolver: missing port after port-separator colon")
)

// The following vars are overridden from tests.
var (
	// TimeAfterFunc is used by the DNS resolver to wait for the given duration
	// to elapse. In non-test code, this is implemented by time.After. In test
	// code, this can be used to control the amount of time the resolver is
	// blocked waiting for the duration to elapse.
	TimeAfterFunc func(time.Duration) <-chan time.Time

	// TimeNowFunc is used by the DNS resolver to get the current time.
	// In non-test code, this is implemented by time.Now. In test code,
	// this can be used to control the current time for the resolver.
	TimeNowFunc func() time.Time

	// TimeUntilFunc is used by the DNS resolver to calculate the remaining
	// wait time for re-resolution. In non-test code, this is implemented by
	// time.Until. In test code, this can be used to control the remaining

View on GitHub (pinned to 0c51461d27)