2018-02-05 21:05:59 +00:00
package client // import "github.com/docker/docker/client"
2016-09-06 18:46:37 +00:00
import (
"bytes"
2018-04-19 22:30:59 +00:00
"context"
2016-09-06 18:46:37 +00:00
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
2016-09-21 21:04:44 +00:00
"os"
2023-05-17 18:36:08 +00:00
"reflect"
2016-09-06 18:46:37 +00:00
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
2019-02-09 14:53:29 +00:00
"github.com/docker/docker/errdefs"
2016-09-08 22:37:45 +00:00
"github.com/pkg/errors"
2016-09-06 18:46:37 +00:00
)
// serverResponse is a wrapper for http API responses.
type serverResponse struct {
body io . ReadCloser
header http . Header
statusCode int
2017-06-26 16:06:34 +00:00
reqURL * url . URL
2016-09-06 18:46:37 +00:00
}
// head sends an http request to the docker API using the method HEAD.
2023-07-10 14:44:59 +00:00
func ( cli * Client ) head ( ctx context . Context , path string , query url . Values , headers http . Header ) ( serverResponse , error ) {
2019-10-12 18:41:14 +00:00
return cli . sendRequest ( ctx , http . MethodHead , path , query , nil , headers )
2016-09-06 18:46:37 +00:00
}
2016-12-01 20:18:02 +00:00
// get sends an http request to the docker API using the method GET with a specific Go context.
2023-07-10 14:44:59 +00:00
func ( cli * Client ) get ( ctx context . Context , path string , query url . Values , headers http . Header ) ( serverResponse , error ) {
2019-10-12 18:41:14 +00:00
return cli . sendRequest ( ctx , http . MethodGet , path , query , nil , headers )
2016-09-06 18:46:37 +00:00
}
2016-12-01 20:18:02 +00:00
// post sends an http request to the docker API using the method POST with a specific Go context.
2023-07-10 14:44:59 +00:00
func ( cli * Client ) post ( ctx context . Context , path string , query url . Values , obj interface { } , headers http . Header ) ( serverResponse , error ) {
2016-10-31 16:39:38 +00:00
body , headers , err := encodeBody ( obj , headers )
if err != nil {
return serverResponse { } , err
}
2019-10-12 18:41:14 +00:00
return cli . sendRequest ( ctx , http . MethodPost , path , query , body , headers )
2016-09-06 18:46:37 +00:00
}
2023-07-10 14:44:59 +00:00
func ( cli * Client ) postRaw ( ctx context . Context , path string , query url . Values , body io . Reader , headers http . Header ) ( serverResponse , error ) {
2019-10-12 18:41:14 +00:00
return cli . sendRequest ( ctx , http . MethodPost , path , query , body , headers )
2016-09-06 18:46:37 +00:00
}
2023-07-10 14:44:59 +00:00
func ( cli * Client ) put ( ctx context . Context , path string , query url . Values , obj interface { } , headers http . Header ) ( serverResponse , error ) {
2021-05-14 17:38:50 +00:00
body , headers , err := encodeBody ( obj , headers )
if err != nil {
return serverResponse { } , err
}
2023-05-17 18:36:08 +00:00
return cli . putRaw ( ctx , path , query , body , headers )
2021-05-14 17:38:50 +00:00
}
2016-12-01 20:18:02 +00:00
// putRaw sends an http request to the docker API using the method PUT.
2023-07-10 14:44:59 +00:00
func ( cli * Client ) putRaw ( ctx context . Context , path string , query url . Values , body io . Reader , headers http . Header ) ( serverResponse , error ) {
2023-05-17 18:36:08 +00:00
// PUT requests are expected to always have a body (apparently)
// so explicitly pass an empty body to sendRequest to signal that
// it should set the Content-Type header if not already present.
if body == nil {
body = http . NoBody
}
2019-10-12 18:41:14 +00:00
return cli . sendRequest ( ctx , http . MethodPut , path , query , body , headers )
2016-09-06 18:46:37 +00:00
}
// delete sends an http request to the docker API using the method DELETE.
2023-07-10 14:44:59 +00:00
func ( cli * Client ) delete ( ctx context . Context , path string , query url . Values , headers http . Header ) ( serverResponse , error ) {
2019-10-12 18:41:14 +00:00
return cli . sendRequest ( ctx , http . MethodDelete , path , query , nil , headers )
2016-09-06 18:46:37 +00:00
}
2023-07-10 14:44:59 +00:00
func encodeBody ( obj interface { } , headers http . Header ) ( io . Reader , http . Header , error ) {
2016-10-31 16:39:38 +00:00
if obj == nil {
return nil , headers , nil
2016-09-06 18:46:37 +00:00
}
2023-05-17 18:36:08 +00:00
// encoding/json encodes a nil pointer as the JSON document `null`,
// irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler.
// That is almost certainly not what the caller intended as the request body.
if reflect . TypeOf ( obj ) . Kind ( ) == reflect . Ptr && reflect . ValueOf ( obj ) . IsNil ( ) {
return nil , headers , nil
}
2016-09-06 18:46:37 +00:00
2016-10-31 16:39:38 +00:00
body , err := encodeData ( obj )
if err != nil {
return nil , headers , err
2016-09-06 18:46:37 +00:00
}
2016-10-31 16:39:38 +00:00
if headers == nil {
headers = make ( map [ string ] [ ] string )
}
headers [ "Content-Type" ] = [ ] string { "application/json" }
return body , headers , nil
}
2016-09-06 18:46:37 +00:00
2023-07-14 16:23:59 +00:00
func ( cli * Client ) buildRequest ( ctx context . Context , method , path string , body io . Reader , headers http . Header ) ( * http . Request , error ) {
req , err := http . NewRequestWithContext ( ctx , method , path , body )
2016-09-06 18:46:37 +00:00
if err != nil {
2016-10-31 16:39:38 +00:00
return nil , err
2016-09-06 18:46:37 +00:00
}
2016-10-31 16:39:38 +00:00
req = cli . addHeaders ( req , headers )
client: define a "dummy" hostname to use for local connections
For local communications (npipe://, unix://), the hostname is not used,
but we need valid and meaningful hostname.
The current code used the client's `addr` as hostname in some cases, which
could contain the path for the unix-socket (`/var/run/docker.sock`), which
gets rejected by go1.20.6 and go1.19.11 because of a security fix for
[CVE-2023-29406 ][1], which was implemented in https://go.dev/issue/60374.
Prior versions go Go would clean the host header, and strip slashes in the
process, but go1.20.6 and go1.19.11 no longer do, and reject the host
header.
This patch introduces a `DummyHost` const, and uses this dummy host for
cases where we don't need an actual hostname.
Before this patch (using go1.20.6):
make GO_VERSION=1.20.6 TEST_FILTER=TestAttach test-integration
=== RUN TestAttachWithTTY
attach_test.go:46: assertion failed: error is not nil: http: invalid Host header
--- FAIL: TestAttachWithTTY (0.11s)
=== RUN TestAttachWithoutTTy
attach_test.go:46: assertion failed: error is not nil: http: invalid Host header
--- FAIL: TestAttachWithoutTTy (0.02s)
FAIL
With this patch applied:
make GO_VERSION=1.20.6 TEST_FILTER=TestAttach test-integration
INFO: Testing against a local daemon
=== RUN TestAttachWithTTY
--- PASS: TestAttachWithTTY (0.12s)
=== RUN TestAttachWithoutTTy
--- PASS: TestAttachWithoutTTy (0.02s)
PASS
[1]: https://github.com/advisories/GHSA-f8f7-69v5-w4vx
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-12 12:15:38 +00:00
req . URL . Scheme = cli . scheme
req . URL . Host = cli . addr
2016-09-06 18:46:37 +00:00
if cli . proto == "unix" || cli . proto == "npipe" {
client: define a "dummy" hostname to use for local connections
For local communications (npipe://, unix://), the hostname is not used,
but we need valid and meaningful hostname.
The current code used the client's `addr` as hostname in some cases, which
could contain the path for the unix-socket (`/var/run/docker.sock`), which
gets rejected by go1.20.6 and go1.19.11 because of a security fix for
[CVE-2023-29406 ][1], which was implemented in https://go.dev/issue/60374.
Prior versions go Go would clean the host header, and strip slashes in the
process, but go1.20.6 and go1.19.11 no longer do, and reject the host
header.
This patch introduces a `DummyHost` const, and uses this dummy host for
cases where we don't need an actual hostname.
Before this patch (using go1.20.6):
make GO_VERSION=1.20.6 TEST_FILTER=TestAttach test-integration
=== RUN TestAttachWithTTY
attach_test.go:46: assertion failed: error is not nil: http: invalid Host header
--- FAIL: TestAttachWithTTY (0.11s)
=== RUN TestAttachWithoutTTy
attach_test.go:46: assertion failed: error is not nil: http: invalid Host header
--- FAIL: TestAttachWithoutTTy (0.02s)
FAIL
With this patch applied:
make GO_VERSION=1.20.6 TEST_FILTER=TestAttach test-integration
INFO: Testing against a local daemon
=== RUN TestAttachWithTTY
--- PASS: TestAttachWithTTY (0.12s)
=== RUN TestAttachWithoutTTy
--- PASS: TestAttachWithoutTTy (0.02s)
PASS
[1]: https://github.com/advisories/GHSA-f8f7-69v5-w4vx
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-07-12 12:15:38 +00:00
// Override host header for non-tcp connections.
req . Host = DummyHost
2016-09-06 18:46:37 +00:00
}
2016-09-09 03:44:25 +00:00
2023-05-17 18:36:08 +00:00
if body != nil && req . Header . Get ( "Content-Type" ) == "" {
2016-09-06 18:46:37 +00:00
req . Header . Set ( "Content-Type" , "text/plain" )
}
2016-10-31 16:39:38 +00:00
return req , nil
}
2023-07-10 14:44:59 +00:00
func ( cli * Client ) sendRequest ( ctx context . Context , method , path string , query url . Values , body io . Reader , headers http . Header ) ( serverResponse , error ) {
2023-07-14 16:23:59 +00:00
req , err := cli . buildRequest ( ctx , method , cli . getAPIPath ( ctx , path , query ) , body , headers )
2016-10-31 16:39:38 +00:00
if err != nil {
return serverResponse { } , err
}
2020-12-10 23:13:37 +00:00
2023-07-14 16:23:59 +00:00
resp , err := cli . doRequest ( req )
2020-12-10 23:13:37 +00:00
switch {
case errors . Is ( err , context . Canceled ) :
return serverResponse { } , errdefs . Cancelled ( err )
case errors . Is ( err , context . DeadlineExceeded ) :
return serverResponse { } , errdefs . Deadline ( err )
case err == nil :
err = cli . checkResponseErr ( resp )
2017-06-26 16:06:34 +00:00
}
2019-02-09 14:53:29 +00:00
return resp , errdefs . FromStatusCode ( err , resp . statusCode )
2016-10-31 16:39:38 +00:00
}
2024-02-23 14:13:22 +00:00
// FIXME(thaJeztah): Should this actually return a serverResp when a connection error occurred?
2023-07-14 16:23:59 +00:00
func ( cli * Client ) doRequest ( req * http . Request ) ( serverResponse , error ) {
2017-06-26 16:06:34 +00:00
serverResp := serverResponse { statusCode : - 1 , reqURL : req . URL }
2016-09-06 18:46:37 +00:00
2018-10-16 16:44:53 +00:00
resp , err := cli . client . Do ( req )
2016-09-06 18:46:37 +00:00
if err != nil {
2016-10-11 22:53:14 +00:00
if cli . scheme != "https" && strings . Contains ( err . Error ( ) , "malformed HTTP response" ) {
2024-02-23 14:13:22 +00:00
return serverResp , errConnectionFailed { fmt . Errorf ( "%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?" , err ) }
2016-09-06 18:46:37 +00:00
}
2016-10-11 22:53:14 +00:00
if cli . scheme == "https" && strings . Contains ( err . Error ( ) , "bad certificate" ) {
2024-02-23 14:13:22 +00:00
return serverResp , errConnectionFailed { errors . Wrap ( err , "the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings" ) }
2016-09-06 18:46:37 +00:00
}
// Don't decorate context sentinel errors; users may be comparing to
// them directly.
2020-08-03 12:59:22 +00:00
if errors . Is ( err , context . Canceled ) || errors . Is ( err , context . DeadlineExceeded ) {
2016-09-06 18:46:37 +00:00
return serverResp , err
}
2023-07-10 14:44:59 +00:00
if uErr , ok := err . ( * url . Error ) ; ok {
if nErr , ok := uErr . Err . ( * net . OpError ) ; ok {
2016-09-21 21:04:44 +00:00
if os . IsPermission ( nErr . Err ) {
2024-02-23 14:13:22 +00:00
return serverResp , errConnectionFailed { errors . Wrapf ( err , "permission denied while trying to connect to the Docker daemon socket at %v" , cli . host ) }
2016-09-21 21:04:44 +00:00
}
}
}
2023-07-10 14:44:59 +00:00
if nErr , ok := err . ( net . Error ) ; ok {
2024-02-23 14:13:22 +00:00
// FIXME(thaJeztah): any net.Error should be considered a connection error (but we should include the original error)?
2023-07-10 14:44:59 +00:00
if nErr . Timeout ( ) {
2016-09-06 18:46:37 +00:00
return serverResp , ErrorConnectionFailed ( cli . host )
}
2023-07-10 14:44:59 +00:00
if strings . Contains ( nErr . Error ( ) , "connection refused" ) || strings . Contains ( nErr . Error ( ) , "dial unix" ) {
2022-01-31 14:59:51 +00:00
return serverResp , ErrorConnectionFailed ( cli . host )
2016-09-06 18:46:37 +00:00
}
}
2016-09-08 22:37:45 +00:00
2016-10-14 17:14:43 +00:00
// Although there's not a strongly typed error for this in go-winio,
// lots of people are using the default configuration for the docker
// daemon on Windows where the daemon is listening on a named pipe
// `//./pipe/docker_engine, and the client must be running elevated.
// Give users a clue rather than the not-overly useful message
2016-11-11 19:51:26 +00:00
// such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.26/info:
2016-10-14 17:14:43 +00:00
// open //./pipe/docker_engine: The system cannot find the file specified.`.
// Note we can't string compare "The system cannot find the file specified" as
// this is localised - for example in French the error would be
// `open //./pipe/docker_engine: Le fichier spécifié est introuvable.`
if strings . Contains ( err . Error ( ) , ` open //./pipe/docker_engine ` ) {
2019-07-22 09:07:50 +00:00
// Checks if client is running with elevated privileges
if f , elevatedErr := os . Open ( "\\\\.\\PHYSICALDRIVE0" ) ; elevatedErr == nil {
2022-07-04 08:07:58 +00:00
err = errors . Wrap ( err , "in the default daemon configuration on Windows, the docker client must be run with elevated privileges to connect" )
2019-07-22 09:07:50 +00:00
} else {
f . Close ( )
2022-07-04 08:07:58 +00:00
err = errors . Wrap ( err , "this error may indicate that the docker daemon is not running" )
2019-07-22 09:07:50 +00:00
}
2016-10-14 17:14:43 +00:00
}
2024-02-23 14:13:22 +00:00
return serverResp , errConnectionFailed { errors . Wrap ( err , "error during connect" ) }
2016-09-06 18:46:37 +00:00
}
if resp != nil {
serverResp . statusCode = resp . StatusCode
2017-06-26 16:06:34 +00:00
serverResp . body = resp . Body
serverResp . header = resp . Header
2016-09-06 18:46:37 +00:00
}
2017-06-26 16:06:34 +00:00
return serverResp , nil
}
2016-09-06 18:46:37 +00:00
2017-06-26 16:06:34 +00:00
func ( cli * Client ) checkResponseErr ( serverResp serverResponse ) error {
if serverResp . statusCode >= 200 && serverResp . statusCode < 400 {
return nil
}
2016-09-06 18:46:37 +00:00
Add HEAD support for /_ping endpoint
Monitoring systems and load balancers are usually configured to use HEAD
requests for health monitoring. The /_ping endpoint currently does not
support this type of request, which means that those systems have fallback
to GET requests.
This patch adds support for HEAD requests on the /_ping endpoint.
Although optional, this patch also returns `Content-Type` and `Content-Length`
headers in case of a HEAD request; Refering to RFC 7231, section 4.3.2:
The HEAD method is identical to GET except that the server MUST NOT
send a message body in the response (i.e., the response terminates at
the end of the header section). The server SHOULD send the same
header fields in response to a HEAD request as it would have sent if
the request had been a GET, except that the payload header fields
(Section 3.3) MAY be omitted. This method can be used for obtaining
metadata about the selected representation without transferring the
representation data and is often used for testing hypertext links for
validity, accessibility, and recent modification.
A payload within a HEAD request message has no defined semantics;
sending a payload body on a HEAD request might cause some existing
implementations to reject the request.
The response to a HEAD request is cacheable; a cache MAY use it to
satisfy subsequent HEAD requests unless otherwise indicated by the
Cache-Control header field (Section 5.2 of [RFC7234]). A HEAD
response might also have an effect on previously cached responses to
GET; see Section 4.3.5 of [RFC7234].
With this patch applied, either `GET` or `HEAD` requests work; the only
difference is that the body is empty in case of a `HEAD` request;
curl -i --unix-socket /var/run/docker.sock http://localhost/_ping
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Docker/dev (linux)
Date: Mon, 14 Jan 2019 12:35:16 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK
curl --head -i --unix-socket /var/run/docker.sock http://localhost/_ping
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Docker/dev (linux)
Date: Mon, 14 Jan 2019 12:34:15 GMT
The client is also updated to use `HEAD` by default, but fallback to `GET`
if the daemon does not support this method.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-01-14 17:08:49 +00:00
var body [ ] byte
var err error
if serverResp . body != nil {
bodyMax := 1 * 1024 * 1024 // 1 MiB
bodyR := & io . LimitedReader {
R : serverResp . body ,
N : int64 ( bodyMax ) ,
}
2021-08-24 10:10:50 +00:00
body , err = io . ReadAll ( bodyR )
Add HEAD support for /_ping endpoint
Monitoring systems and load balancers are usually configured to use HEAD
requests for health monitoring. The /_ping endpoint currently does not
support this type of request, which means that those systems have fallback
to GET requests.
This patch adds support for HEAD requests on the /_ping endpoint.
Although optional, this patch also returns `Content-Type` and `Content-Length`
headers in case of a HEAD request; Refering to RFC 7231, section 4.3.2:
The HEAD method is identical to GET except that the server MUST NOT
send a message body in the response (i.e., the response terminates at
the end of the header section). The server SHOULD send the same
header fields in response to a HEAD request as it would have sent if
the request had been a GET, except that the payload header fields
(Section 3.3) MAY be omitted. This method can be used for obtaining
metadata about the selected representation without transferring the
representation data and is often used for testing hypertext links for
validity, accessibility, and recent modification.
A payload within a HEAD request message has no defined semantics;
sending a payload body on a HEAD request might cause some existing
implementations to reject the request.
The response to a HEAD request is cacheable; a cache MAY use it to
satisfy subsequent HEAD requests unless otherwise indicated by the
Cache-Control header field (Section 5.2 of [RFC7234]). A HEAD
response might also have an effect on previously cached responses to
GET; see Section 4.3.5 of [RFC7234].
With this patch applied, either `GET` or `HEAD` requests work; the only
difference is that the body is empty in case of a `HEAD` request;
curl -i --unix-socket /var/run/docker.sock http://localhost/_ping
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Docker/dev (linux)
Date: Mon, 14 Jan 2019 12:35:16 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK
curl --head -i --unix-socket /var/run/docker.sock http://localhost/_ping
HTTP/1.1 200 OK
Api-Version: 1.40
Cache-Control: no-cache, no-store, must-revalidate
Content-Length: 0
Content-Type: text/plain; charset=utf-8
Docker-Experimental: false
Ostype: linux
Pragma: no-cache
Server: Docker/dev (linux)
Date: Mon, 14 Jan 2019 12:34:15 GMT
The client is also updated to use `HEAD` by default, but fallback to `GET`
if the daemon does not support this method.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-01-14 17:08:49 +00:00
if err != nil {
return err
}
if bodyR . N == 0 {
return fmt . Errorf ( "request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version" , http . StatusText ( serverResp . statusCode ) , bodyMax , serverResp . reqURL )
}
2018-10-10 16:37:39 +00:00
}
2017-06-26 16:06:34 +00:00
if len ( body ) == 0 {
2017-09-08 16:04:34 +00:00
return fmt . Errorf ( "request returned %s for API route and version %s, check if the server supports the requested API version" , http . StatusText ( serverResp . statusCode ) , serverResp . reqURL )
2017-06-26 16:06:34 +00:00
}
2016-09-06 18:46:37 +00:00
2023-08-08 12:54:35 +00:00
var daemonErr error
2023-07-17 10:22:08 +00:00
if serverResp . header . Get ( "Content-Type" ) == "application/json" && ( cli . version == "" || versions . GreaterThan ( cli . version , "1.23" ) ) {
2017-06-26 16:06:34 +00:00
var errorResponse types . ErrorResponse
if err := json . Unmarshal ( body , & errorResponse ) ; err != nil {
2018-12-31 14:33:40 +00:00
return errors . Wrap ( err , "Error reading JSON" )
2017-06-26 16:06:34 +00:00
}
2023-08-08 12:54:35 +00:00
daemonErr = errors . New ( strings . TrimSpace ( errorResponse . Message ) )
2017-06-26 16:06:34 +00:00
} else {
2023-08-08 12:54:35 +00:00
daemonErr = errors . New ( strings . TrimSpace ( string ( body ) ) )
2017-06-26 16:06:34 +00:00
}
2023-08-08 12:54:35 +00:00
return errors . Wrap ( daemonErr , "Error response from daemon" )
2016-09-06 18:46:37 +00:00
}
2023-07-10 14:44:59 +00:00
func ( cli * Client ) addHeaders ( req * http . Request , headers http . Header ) * http . Request {
2016-09-06 18:46:37 +00:00
// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
// then the user can't change OUR headers
for k , v := range cli . customHTTPHeaders {
2020-09-30 10:56:42 +00:00
if versions . LessThan ( cli . version , "1.25" ) && http . CanonicalHeaderKey ( k ) == "User-Agent" {
2016-11-03 00:43:32 +00:00
continue
}
2016-09-06 18:46:37 +00:00
req . Header . Set ( k , v )
}
2021-04-16 14:15:10 +00:00
for k , v := range headers {
2020-09-30 10:56:42 +00:00
req . Header [ http . CanonicalHeaderKey ( k ) ] = v
2016-09-06 18:46:37 +00:00
}
2023-06-01 20:42:46 +00:00
if cli . userAgent != nil {
if * cli . userAgent == "" {
req . Header . Del ( "User-Agent" )
} else {
req . Header . Set ( "User-Agent" , * cli . userAgent )
}
}
2016-10-31 16:39:38 +00:00
return req
2016-09-06 18:46:37 +00:00
}
func encodeData ( data interface { } ) ( * bytes . Buffer , error ) {
params := bytes . NewBuffer ( nil )
if data != nil {
if err := json . NewEncoder ( params ) . Encode ( data ) ; err != nil {
return nil , err
}
}
return params , nil
}
func ensureReaderClosed ( response serverResponse ) {
2017-06-26 16:06:34 +00:00
if response . body != nil {
2016-09-06 18:46:37 +00:00
// Drain up to 512 bytes and close the body to let the Transport reuse the connection
2021-08-24 10:10:50 +00:00
io . CopyN ( io . Discard , response . body , 512 )
2016-09-06 18:46:37 +00:00
response . body . Close ( )
}
}