{"record":{"id":"06c90670ab39bacb","repo":"juanfont/headscale","slug":"reading-pull-output-w","errorCode":null,"errorMessage":"reading pull output: %w","messagePattern":"reading pull output: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/hi/docker.go","lineNumber":571,"sourceCode":"\t\tfunc() (struct{}, error) {\n\t\t\treader, pullErr := cli.ImagePull(ctx, imageName, image.PullOptions{RegistryAuth: registryAuth})\n\t\t\tif pullErr != nil {\n\t\t\t\tif isPermanentDockerPullError(pullErr) {\n\t\t\t\t\treturn struct{}{}, backoff.Permanent(pullErr)\n\t\t\t\t}\n\n\t\t\t\treturn struct{}{}, fmt.Errorf(\"pulling image %s: %w\", imageName, pullErr)\n\t\t\t}\n\t\t\tdefer reader.Close()\n\n\t\t\tsink := io.Discard\n\t\t\tif verbose {\n\t\t\t\tsink = os.Stdout\n\t\t\t}\n\n\t\t\t_, copyErr := io.Copy(sink, reader)\n\t\t\tif copyErr != nil {\n\t\t\t\treturn struct{}{}, fmt.Errorf(\"reading pull output: %w\", copyErr)\n\t\t\t}\n\n\t\t\treturn struct{}{}, nil\n\t\t},\n\t\tbackoff.WithBackOff(backoff.NewExponentialBackOff()),\n\t\tbackoff.WithMaxElapsedTime(60*time.Second),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !verbose {\n\t\tlog.Printf(\"Image %s pulled successfully\", imageName)\n\t}\n\n\treturn nil\n}\n","sourceCodeStart":553,"sourceCodeEnd":589,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/cmd/hi/docker.go#L553-L589","documentation":"Returned inside `ensureImageAvailable`'s retry closure when the pull request succeeded but reading the pull output stream (`io.Copy` from the response reader to os.Stdout or io.Discard) failed. Docker's pull API delivers progress over this stream; a broken stream can also abort the pull server-side. It is retried under the same 60s backoff budget.","triggerScenarios":"Connection reset mid-pull; proxy or VPN dropping long-lived chunked responses; daemon restarting while streaming layers; context cancellation arriving during the copy.","commonSituations":"Flaky networks pulling large golang images; corporate proxies with short idle timeouts; CI egress filtering that truncates streaming responses.","solutions":["Pre-pull manually (`docker pull golang:<version>`) outside the test run so `hi` finds it locally and skips pulling entirely.","Fix proxy/VPN stability for chunked streams.","Re-run — transient resets are what the backoff exists for, though the budget is 60s.","Check daemon logs if the stream breaks repeatedly at the same layer."],"exampleFix":"# before: relying on hi to pull over a flaky proxy\ngo run ./cmd/hi run TestX   # reading pull output: connection reset\n\n# after: pre-pull separately, then run (image found locally)\ndocker pull golang:1.26.1 && go run ./cmd/hi run TestX","handlingStrategy":"retry","validationCode":"// eliminate the streaming path entirely by pre-pulling\nif err := exec.Command(\"docker\", \"pull\", \"golang:\"+cfg.GoVersion).Run(); err != nil {\n    return fmt.Errorf(\"pre-pull failed: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if err := ensureImageAvailable(ctx, cli, image, verbose); err != nil {\n    if strings.Contains(err.Error(), \"reading pull output\") {\n        // stream broke mid-pull: pre-pull manually to resume/complete layers, then re-run\n    }\n}","preventionTips":["Pre-pull large base images in a separate CI step.","Avoid proxies that truncate long-lived chunked responses for registry traffic.","Keep the network stable through the pull window."],"tags":["docker","image-pull","streaming","network"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}