{"record":{"id":"d715bfec2e9248d5","repo":"docker/compose","slug":"docker-client-returned-empty-version-after-success","errorCode":null,"errorMessage":"docker client returned empty version after successful API negotiation","messagePattern":"docker client returned empty version after successful API negotiation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/compose/compose.go","lineNumber":526,"sourceCode":"//\n// After negotiation, Compose should never rely on features or request attributes\n// not defined by this API version, even if the daemon's raw version is higher.\nfunc (s *composeService) RuntimeAPIVersion(ctx context.Context) (string, error) {\n\ts.runtimeAPIVersion.mu.Lock()\n\tdefer s.runtimeAPIVersion.mu.Unlock()\n\tif s.runtimeAPIVersion.val != \"\" {\n\t\treturn s.runtimeAPIVersion.val, nil\n\t}\n\n\tcli := s.apiClient()\n\t_, err := cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tversion := cli.ClientVersion()\n\tif version == \"\" {\n\t\treturn \"\", fmt.Errorf(\"docker client returned empty version after successful API negotiation\")\n\t}\n\n\ts.runtimeAPIVersion.val = version\n\treturn s.runtimeAPIVersion.val, nil\n}\n","sourceCodeStart":508,"sourceCodeEnd":532,"githubUrl":"https://github.com/docker/compose/blob/ddc4b044b62e9f715212ea4143fa830fac76382f/pkg/compose/compose.go#L508-L532","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["If embedding, construct the docker CLI via command.NewDockerCli() so negotiation populates the version","In tests, make the fake client's Ping/ClientVersion behave: return a semver like '1.44' from ClientVersion() after Ping","If you manually set a client version, set a concrete one (e.g. api.DefaultVersion) instead of an empty string"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if v := dockerCli.Client().ClientVersion(); v == \"\" {\n    // force a concrete version instead of relying on negotiation\n    // dockerCli.Client().UpdateClientVersion(api.DefaultVersion) or rebuild the CLI via command.NewDockerCli\n}","typeGuard":null,"tryCatchPattern":"v, err := composeAPI.RuntimeAPIVersion(ctx)\nif err != nil && strings.Contains(err.Error(), \"empty version\") {\n    // client misconfiguration, not a daemon problem: rebuild the docker CLI and retry once\n}","preventionTips":["When embedding compose, always create the docker CLI with command.NewDockerCli so Ping negotiation works","In fakes/tests, implement ClientVersion() to return a valid version like '1.44'"],"tags":["docker-client","api-version","embedding","negotiation"],"backgroundTag":null,"analyzedSha":"ddc4b044b62e9f715212ea4143fa830fac76382f","analyzedAt":"2026-08-15T13:31:42.319Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}