wtfutil/wtf · error

could not create client: %w

Error message

could not create client: %w

What it means

NewWidget builds the Docker client with client.NewClientWithOpts(client.FromEnv), which reads DOCKER_HOST, DOCKER_TLS_VERIFY, DOCKER_CERT_PATH etc. from the environment. If client construction fails (invalid DOCKER_HOST URL, missing/malformed TLS config, unparsable env), the error is stored in the widget's displayBuffer as "could not create client". This happens before any daemon call — it is a configuration failure.

Source

Thrown at modules/docker/widget.go:28

type Widget struct {
	view.TextWidget
	cli           *client.Client
	settings      *Settings
	displayBuffer string
}

func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget {
	widget := Widget{
		TextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.Common),
		settings:   settings,
	}

	widget.View.SetScrollable(true)

	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		widget.displayBuffer = fmt.Errorf("could not create client: %w", err).Error()
	} else {
		widget.cli = cli
	}

	widget.refreshDisplayBuffer()

	return &widget
}

/* -------------------- Exported Functions -------------------- */

func (widget *Widget) Refresh() {
	widget.refreshDisplayBuffer()
	widget.Redraw(widget.display)
}

/* -------------------- Unexported Functions -------------------- */

View on GitHub (pinned to bb838c1ccb)

Solutions

  1. Validate DOCKER_HOST parses as a URL: docker -H "$DOCKER_HOST" info.
  2. Fix the scheme — use tcp://, unix://, or npipe:// prefixes explicitly.
  3. If DOCKER_TLS_VERIFY=1, confirm DOCKER_CERT_PATH contains ca.pem, cert.pem, key.pem.
  4. Unset DOCKER_* vars to fall back to the default local socket if you intend local usage.

Example fix

// before
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
	widget.displayBuffer = fmt.Errorf("could not create client: %w", err).Error()
}
// after
host := os.Getenv("DOCKER_HOST")
if host != "" && !strings.Contains(host, "://") {
	host = "tcp://" + host
	os.Setenv("DOCKER_HOST", host)
}
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
	widget.displayBuffer = fmt.Errorf("could not create client: %w", err).Error()
}
Defensive patterns

Strategy: validation

Validate before calling

host := os.Getenv("DOCKER_HOST")
if host != "" {
	if _, err := url.Parse(host); err != nil || !strings.Contains(host, "://") {
		return fmt.Errorf("invalid DOCKER_HOST %q: must be a URL like tcp://host:2375 or unix:///path", host)
	}
}
if os.Getenv("DOCKER_TLS_VERIFY") != "" {
	for _, f := range []string{"ca.pem", "cert.pem", "key.pem"} {
		if _, err := os.Stat(filepath.Join(os.Getenv("DOCKER_CERT_PATH"), f)); err != nil {
			return fmt.Errorf("missing TLS cert %s in DOCKER_CERT_PATH", f)
		}
	}
}

Type guard

func validDockerHost(host string) bool {
	u, err := url.Parse(host)
	return err == nil && u.Scheme != "" && (u.Scheme == "unix" || u.Scheme == "tcp" || u.Scheme == "npipe" || u.Scheme == "ssh")
}

Try / catch

cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
	widget.displayBuffer = fmt.Sprintf("docker client config invalid: %v — check DOCKER_HOST/DOCKER_CERT_PATH", err)
	return
}

Prevention

When it happens

Trigger: client.FromEnv fails: DOCKER_HOST is not a valid URL (e.g., "myhost" instead of "tcp://myhost:2375"), DOCKER_CERT_PATH files missing/unreadable with DOCKER_TLS_VERIFY=1, or unsupported scheme.

Common situations: Copy-pasted DOCKER_HOST from a teammate with a typo, switched from Docker Desktop to colima/podman without updating the socket path, missing cert files after machine migration, or env vars set in one shell but not the one launching the app.

Related errors


AI-assisted analysis of wtfutil/wtf@bb838c1ccb (2026-09-03). Data as JSON: /api/errors/9980bb886076b006. Report an issue: GitHub.