docker/compose · error

docker client returned empty version after successful API ne

Error message

docker client returned empty version after successful API negotiation

What it means

RuntimeAPIVersion pings the daemon with NegotiateAPIVersion and then reads cli.ClientVersion(). A successful ping should always leave a negotiated version, so an empty string means the client was constructed or overridden in a way that bypasses negotiation (custom client wrapper, mock, or manual version pinning). The guard exists because downstream version parsing would panic or misbehave on an empty version.

Source

Thrown at pkg/compose/compose.go:526

//
// After negotiation, Compose should never rely on features or request attributes
// not defined by this API version, even if the daemon's raw version is higher.
func (s *composeService) RuntimeAPIVersion(ctx context.Context) (string, error) {
	s.runtimeAPIVersion.mu.Lock()
	defer s.runtimeAPIVersion.mu.Unlock()
	if s.runtimeAPIVersion.val != "" {
		return s.runtimeAPIVersion.val, nil
	}

	cli := s.apiClient()
	_, err := cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true})
	if err != nil {
		return "", err
	}

	version := cli.ClientVersion()
	if version == "" {
		return "", fmt.Errorf("docker client returned empty version after successful API negotiation")
	}

	s.runtimeAPIVersion.val = version
	return s.runtimeAPIVersion.val, nil
}

View on GitHub (pinned to ddc4b044b6)

Solutions

  1. If embedding, construct the docker CLI via command.NewDockerCli() so negotiation populates the version
  2. In tests, make the fake client's Ping/ClientVersion behave: return a semver like '1.44' from ClientVersion() after Ping
  3. If you manually set a client version, set a concrete one (e.g. api.DefaultVersion) instead of an empty string
Defensive patterns

Strategy: try-catch

Validate before calling

if v := dockerCli.Client().ClientVersion(); v == "" {
    // force a concrete version instead of relying on negotiation
    // dockerCli.Client().UpdateClientVersion(api.DefaultVersion) or rebuild the CLI via command.NewDockerCli
}

Try / catch

v, err := composeAPI.RuntimeAPIVersion(ctx)
if err != nil && strings.Contains(err.Error(), "empty version") {
    // client misconfiguration, not a daemon problem: rebuild the docker CLI and retry once
}

Prevention

When it happens

Trigger: Programmatic use of compose-service where the docker CLI object returns an empty ClientVersion() after Ping — custom command.Cli implementations, test doubles, or a client whose version was explicitly set to '' before negotiation; effectively unreachable against a real dockerd.

Common situations: Embedding docker/compose as a library with a wrapped/mock docker CLI; unit tests with fake clients that answer Ping but not version negotiation; tooling that builds its own client APIClient and injects it via WithStreams-style options.


AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15). Data as JSON: /api/errors/d715bfec2e9248d5. Report an issue: GitHub.