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
- Pass host:port only: new grpc.Client('grpc.acme.com:443', opts)
- Strip any scheme before constructing: target = target.replace(/^\w+:\/\//, '')
- 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
- Never include a scheme in the grpc.Client target
- Always append the port (443 for TLS, 50051-style for h2c)
- Bracket IPv6 literals in host:port targets
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
- grpc target host cannot be empty
- grpc: refusing to dial without executionId
- grpc: dialers not initialized for executionId %q
- failed to build request parser: %w
- grpc status %s: %s
AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15).
Data as JSON: /api/errors/a318c07271eabe60.
Report an issue: GitHub.