doRequest sends an HTTP request and returns an HTTP response. It is a wrapper around [http.Client.Do] with extra handling to decorate errors. Otherwise, it behaves identical to [http.Client.Do]; an error is returned when failing to make a connection, On error, any Response can be ignored. A non-2xx
(req *http.Request)
| 128 | // when failing to make a connection, On error, any Response can be ignored. |
| 129 | // A non-2xx status code doesn't cause an error. |
| 130 | func (cli *Client) doRequest(req *http.Request) (*http.Response, error) { |
| 131 | resp, err := cli.client.Do(req) // #nosec G704 -- ignore "SSRF via taint analysis"; API client intentionally sends caller-provided requests/URLs. |
| 132 | if err == nil { |
| 133 | return resp, nil |
| 134 | } |
| 135 | |
| 136 | if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { |
| 137 | return nil, errConnectionFailed{fmt.Errorf("%w.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)} |
| 138 | } |
| 139 | |
| 140 | const ( |
| 141 | // Go 1.25 / TLS 1.3 may produce a generic "handshake failure" |
| 142 | // whereas TLS 1.2 may produce a "bad certificate" TLS alert. |
| 143 | // See https://github.com/golang/go/issues/56371 |
| 144 | // |
| 145 | // > https://tip.golang.org/doc/go1.12#tls_1_3 |
| 146 | // > |
| 147 | // > In TLS 1.3 the client is the last one to speak in the handshake, so if |
| 148 | // > it causes an error to occur on the server, it will be returned on the |
| 149 | // > client by the first Read, not by Handshake. For example, that will be |
| 150 | // > the case if the server rejects the client certificate. |
| 151 | // |
| 152 | // https://github.com/golang/go/blob/go1.25.1/src/crypto/tls/alert.go#L71-L72 |
| 153 | alertBadCertificate = "bad certificate" // go1.24 / TLS 1.2 |
| 154 | alertHandshakeFailure = "handshake failure" // go1.25 / TLS 1.3 |
| 155 | ) |
| 156 | |
| 157 | // TODO(thaJeztah): see if we can use errors.As for a [crypto/tls.AlertError] instead of bare string matching. |
| 158 | if cli.scheme == "https" && (strings.Contains(err.Error(), alertHandshakeFailure) || strings.Contains(err.Error(), alertBadCertificate)) { |
| 159 | return nil, errConnectionFailed{fmt.Errorf("the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings: %w", err)} |
| 160 | } |
| 161 | |
| 162 | // Don't decorate context sentinel errors; users may be comparing to |
| 163 | // them directly. |
| 164 | if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { |
| 165 | return nil, err |
| 166 | } |
| 167 | |
| 168 | if errors.Is(err, os.ErrPermission) { |
| 169 | // Don't include request errors (Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.51/version"), |
| 170 | // which are irrelevant if we weren't able to connect. |
| 171 | return nil, errConnectionFailed{fmt.Errorf("permission denied while trying to connect to the docker API at %v", cli.host)} |
| 172 | } |
| 173 | if errors.Is(err, os.ErrNotExist) { |
| 174 | // Unwrap the error to remove request errors (Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.51/version"), |
| 175 | // which are irrelevant if we weren't able to connect. |
| 176 | err = errors.Unwrap(err) |
| 177 | return nil, errConnectionFailed{fmt.Errorf("failed to connect to the docker API at %v; check if the path is correct and if the daemon is running: %w", cli.host, err)} |
| 178 | } |
| 179 | var dnsErr *net.DNSError |
| 180 | if errors.As(err, &dnsErr) { |
| 181 | return nil, errConnectionFailed{fmt.Errorf("failed to connect to the docker API at %v: %w", cli.host, dnsErr)} |
| 182 | } |
| 183 | |
| 184 | var nErr net.Error |
| 185 | if errors.As(err, &nErr) { |
| 186 | // FIXME(thaJeztah): any net.Error should be considered a connection error (but we should include the original error)? |
| 187 | if nErr.Timeout() { |