amir20/dozzle · error

cloud: dial

Error message

cloud: dial: %w

What it means

unaryServiceClient creates the lazily-initialized gRPC client connection to the cloud service with grpc.NewClient and wraps any dial/client-construction failure as 'cloud: dial: %w'. Without this connection, both GetAlerts and SearchLogs cannot run. TLS credentials are chosen based on whether the target is insecure.

Solutions

  1. Check the cloud target/address configuration for typos and unsupported schemes
  2. Confirm the target format is valid for grpc.NewClient (e.g. dns:///host:port or host:port)
  3. Verify TLS expectations: if the endpoint is plaintext, the insecure path must be selected correctly
  4. Test endpoint reachability with a simple gRPC client or grpcurl before app startup

Example fix

// before
target := os.Getenv("DOZZLE_CLOUD_URL") // e.g. "https://cloud.example.com"
// after: strip scheme and use grpc-friendly target
target := strings.TrimPrefix(os.Getenv("DOZZLE_CLOUD_URL"), "https://")
conn, err := grpc.NewClient(target, creds)
Defensive patterns

Strategy: validation

Validate before calling

target := strings.TrimSpace(cfg.CloudTarget)
if target == "" || strings.Contains(target, " ") || !strings.Contains(target, ":") {
    return fmt.Errorf("invalid cloud target: %q", target)
}

Try / catch

cl, err := unaryServiceClient(ctx)
if err != nil {
    return nil, fmt.Errorf("cloud unavailable (%v), check DOZZLE_CLOUD target config", err)
}

Prevention

When it happens

Trigger: grpc.NewClient returns an error for the configured target: malformed target/address string, invalid target scheme, or credentials setup failure when creating the connection.

Common situations: Misconfigured cloud endpoint URL (bad scheme or host), environment variable pointing at a wrong address, target string with stray whitespace or unsupported prefix.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of amir20/dozzle@d9463cbe21 (2026-09-07). Data as JSON: /api/errors/bbea590dc10effd7. Report an issue: GitHub.

Appendix: source

Thrown at internal/cloud/search.go:65

// unaryServiceClient returns a (lazily dialed) reusable gRPC client. The
// underlying conn is shared across every Dozzle-initiated unary call so we pay
// the TLS handshake once per process — not once per keystroke or scroll.
func (c *Client) unaryServiceClient() (pb.CloudToolServiceClient, error) {
	c.unaryConnMu.Lock()
	defer c.unaryConnMu.Unlock()
	if c.unaryClient != nil {
		return c.unaryClient, nil
	}
	var creds grpc.DialOption
	if c.plaintext {
		creds = grpc.WithTransportCredentials(insecure.NewCredentials())
	} else {
		creds = grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, ""))
	}
	conn, err := grpc.NewClient(c.target, creds)
	if err != nil {
		return nil, fmt.Errorf("cloud: dial: %w", err)
	}
	c.unaryConn = conn
	c.unaryClient = pb.NewCloudToolServiceClient(conn)
	return c.unaryClient, nil
}

// SearchLogs runs a Cloud-side log search against the existing gRPC service.
// Reuses a long-lived gRPC conn (lazily dialed on first call) so the
// 500ms search timeout isn't burned on a TLS handshake per keystroke.
// Identity (user, instance) is enforced server-side from the authenticated
// metadata; this client passes only the per-request fields below.
func (c *Client) SearchLogs(ctx context.Context, query string, limit int32, hostID, containerID string, before int64) (*SearchLogResult, error) {
	apiKey := c.apiKeyFunc()
	if apiKey == "" {
		return nil, ErrNotConfigured
	}

	client, err := c.unaryServiceClient()

View on GitHub (pinned to d9463cbe21)