projectdiscovery/nuclei · error

invalid grpc target %q (expected host:port): %w

Error message

invalid grpc target %q (expected host:port): %w

What it means

grpc's dialTarget validates the target with net.SplitHostPort and it failed: the Client target must be plain host:port ('grpc.acme.com:443'). Scheme prefixes and port-less hosts are rejected up front so the passthrough resolver can hand the address verbatim to fastdialer, keeping DNS resolution and policy enforcement inside nuclei.

Source

Thrown at pkg/js/libs/grpc/invoke.go:45

// logic stays independently testable.
type connConfig struct {
	plaintext          bool
	insecureSkipVerify bool
	serverName         string
	maxRecvMsgSize     int
}

// dialTarget builds a *grpc.ClientConn whose every connection is routed through
// nuclei's network policy. The host is validated up front and the actual dial
// is delegated to the execution's fastdialer via a custom context dialer, so
// IP/host denylists and RestrictLocalNetworkAccess are always enforced. The
// passthrough scheme guarantees the target is handed verbatim to our dialer
// (instead of gRPC's built in DNS resolver), keeping resolution and policy
// enforcement inside fastdialer.
func dialTarget(ctx context.Context, executionID, target string, cfg connConfig) (*grpc.ClientConn, error) {
	host, _, err := net.SplitHostPort(target)
	if err != nil {
		return nil, fmt.Errorf("invalid grpc target %q (expected host:port): %w", target, err)
	}
	if host == "" {
		return nil, fmt.Errorf("grpc target host cannot be empty")
	}
	if executionID == "" {
		return nil, fmt.Errorf("grpc: refusing to dial without executionId")
	}
	if !protocolstate.IsHostAllowed(executionID, host) {
		return nil, protocolstate.ErrHostDenied.Msgf(host)
	}
	dialers := protocolstate.GetDialersWithId(executionID)
	if dialers == nil || dialers.Fastdialer == nil {
		return nil, fmt.Errorf("grpc: dialers not initialized for executionId %q", executionID)
	}

	contextDialer := func(dialCtx context.Context, addr string) (net.Conn, error) {
		return dialers.Fastdialer.Dial(dialCtx, "tcp", addr)
	}

View on GitHub (pinned to 265b3a3dec)

Solutions

  1. Pass host:port only: new grpc.Client('grpc.acme.com:443', opts)
  2. Strip any scheme before constructing: target = target.replace(/^\w+:\/\//, '')
  3. Bracket IPv6 targets: '[2001:db8::1]:443'

Example fix

// before
const client = new grpc.Client('grpc://grpc.acme.com:443');

// after: plain host:port, no scheme
const client = new grpc.Client('grpc.acme.com:443');
Defensive patterns

Strategy: validation

Validate before calling

const HOST_PORT = /^(?:\[[0-9a-fA-F:]+\]|[^:/\s]+):\d+$/;
function assertHostPort(t) {
  if (!HOST_PORT.test(t)) throw new Error(`grpc target must be host:port, got: ${t}`);
}
assertHostPort(target);
const client = new grpc.Client(target, opts);

Type guard

const isHostPort = (t) => /^(?:\[[0-9a-fA-F:]+\]|[^:/\s]+):\d+$/.test(String(t || ''));

Try / catch

try {
  const c = new grpc.Client(target);
} catch (e) {
  if (/invalid grpc target/.test(e.message || '')) {
    // strip scheme, add port, then reconstruct the client
  }
}

Prevention

When it happens

Trigger: new grpc.Client('grpc.acme.com') with no :443; passing 'grpc://grpc.acme.com:443' or 'https://grpc.acme.com:443'; unbracketed IPv6 such as '::1:443'; a target string with whitespace.

Common situations: Copy-pasting a URL from documentation into the Client constructor; template variables that carry a scheme; authors assuming grpc.Client behaves like http.Client (which requires a scheme — grpc is the opposite).

Related errors


AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15). Data as JSON: /api/errors/a318c07271eabe60. Report an issue: GitHub.