mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
724 lines
19 KiB
724 lines
19 KiB
package api |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"crypto/tls" |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"log" |
|
"net" |
|
"net/http" |
|
"net/url" |
|
"os" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/hashicorp/go-cleanhttp" |
|
"github.com/hashicorp/go-rootcerts" |
|
) |
|
|
|
const ( |
|
// HTTPAddrEnvName defines an environment variable name which sets |
|
// the HTTP address if there is no -http-addr specified. |
|
HTTPAddrEnvName = "CONSUL_HTTP_ADDR" |
|
|
|
// HTTPTokenEnvName defines an environment variable name which sets |
|
// the HTTP token. |
|
HTTPTokenEnvName = "CONSUL_HTTP_TOKEN" |
|
|
|
// HTTPAuthEnvName defines an environment variable name which sets |
|
// the HTTP authentication header. |
|
HTTPAuthEnvName = "CONSUL_HTTP_AUTH" |
|
|
|
// HTTPSSLEnvName defines an environment variable name which sets |
|
// whether or not to use HTTPS. |
|
HTTPSSLEnvName = "CONSUL_HTTP_SSL" |
|
|
|
// HTTPCAFile defines an environment variable name which sets the |
|
// CA file to use for talking to Consul over TLS. |
|
HTTPCAFile = "CONSUL_CACERT" |
|
|
|
// HTTPCAPath defines an environment variable name which sets the |
|
// path to a directory of CA certs to use for talking to Consul over TLS. |
|
HTTPCAPath = "CONSUL_CAPATH" |
|
|
|
// HTTPClientCert defines an environment variable name which sets the |
|
// client cert file to use for talking to Consul over TLS. |
|
HTTPClientCert = "CONSUL_CLIENT_CERT" |
|
|
|
// HTTPClientKey defines an environment variable name which sets the |
|
// client key file to use for talking to Consul over TLS. |
|
HTTPClientKey = "CONSUL_CLIENT_KEY" |
|
|
|
// HTTPTLSServerName defines an environment variable name which sets the |
|
// server name to use as the SNI host when connecting via TLS |
|
HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME" |
|
|
|
// HTTPSSLVerifyEnvName defines an environment variable name which sets |
|
// whether or not to disable certificate checking. |
|
HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY" |
|
) |
|
|
|
// QueryOptions are used to parameterize a query |
|
type QueryOptions struct { |
|
// Providing a datacenter overwrites the DC provided |
|
// by the Config |
|
Datacenter string |
|
|
|
// AllowStale allows any Consul server (non-leader) to service |
|
// a read. This allows for lower latency and higher throughput |
|
AllowStale bool |
|
|
|
// RequireConsistent forces the read to be fully consistent. |
|
// This is more expensive but prevents ever performing a stale |
|
// read. |
|
RequireConsistent bool |
|
|
|
// WaitIndex is used to enable a blocking query. Waits |
|
// until the timeout or the next index is reached |
|
WaitIndex uint64 |
|
|
|
// WaitTime is used to bound the duration of a wait. |
|
// Defaults to that of the Config, but can be overridden. |
|
WaitTime time.Duration |
|
|
|
// Token is used to provide a per-request ACL token |
|
// which overrides the agent's default token. |
|
Token string |
|
|
|
// Near is used to provide a node name that will sort the results |
|
// in ascending order based on the estimated round trip time from |
|
// that node. Setting this to "_agent" will use the agent's node |
|
// for the sort. |
|
Near string |
|
|
|
// NodeMeta is used to filter results by nodes with the given |
|
// metadata key/value pairs. Currently, only one key/value pair can |
|
// be provided for filtering. |
|
NodeMeta map[string]string |
|
|
|
// RelayFactor is used in keyring operations to cause reponses to be |
|
// relayed back to the sender through N other random nodes. Must be |
|
// a value from 0 to 5 (inclusive). |
|
RelayFactor uint8 |
|
} |
|
|
|
// WriteOptions are used to parameterize a write |
|
type WriteOptions struct { |
|
// Providing a datacenter overwrites the DC provided |
|
// by the Config |
|
Datacenter string |
|
|
|
// Token is used to provide a per-request ACL token |
|
// which overrides the agent's default token. |
|
Token string |
|
|
|
// RelayFactor is used in keyring operations to cause reponses to be |
|
// relayed back to the sender through N other random nodes. Must be |
|
// a value from 0 to 5 (inclusive). |
|
RelayFactor uint8 |
|
} |
|
|
|
// QueryMeta is used to return meta data about a query |
|
type QueryMeta struct { |
|
// LastIndex. This can be used as a WaitIndex to perform |
|
// a blocking query |
|
LastIndex uint64 |
|
|
|
// Time of last contact from the leader for the |
|
// server servicing the request |
|
LastContact time.Duration |
|
|
|
// Is there a known leader |
|
KnownLeader bool |
|
|
|
// How long did the request take |
|
RequestTime time.Duration |
|
|
|
// Is address translation enabled for HTTP responses on this agent |
|
AddressTranslationEnabled bool |
|
} |
|
|
|
// WriteMeta is used to return meta data about a write |
|
type WriteMeta struct { |
|
// How long did the request take |
|
RequestTime time.Duration |
|
} |
|
|
|
// HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication |
|
type HttpBasicAuth struct { |
|
// Username to use for HTTP Basic Authentication |
|
Username string |
|
|
|
// Password to use for HTTP Basic Authentication |
|
Password string |
|
} |
|
|
|
// Config is used to configure the creation of a client |
|
type Config struct { |
|
// Address is the address of the Consul server |
|
Address string |
|
|
|
// Scheme is the URI scheme for the Consul server |
|
Scheme string |
|
|
|
// Datacenter to use. If not provided, the default agent datacenter is used. |
|
Datacenter string |
|
|
|
// Transport is the Transport to use for the http client. |
|
Transport *http.Transport |
|
|
|
// HttpClient is the client to use. Default will be |
|
// used if not provided. |
|
HttpClient *http.Client |
|
|
|
// HttpAuth is the auth info to use for http access. |
|
HttpAuth *HttpBasicAuth |
|
|
|
// WaitTime limits how long a Watch will block. If not provided, |
|
// the agent default values will be used. |
|
WaitTime time.Duration |
|
|
|
// Token is used to provide a per-request ACL token |
|
// which overrides the agent's default token. |
|
Token string |
|
|
|
TLSConfig TLSConfig |
|
} |
|
|
|
// TLSConfig is used to generate a TLSClientConfig that's useful for talking to |
|
// Consul using TLS. |
|
type TLSConfig struct { |
|
// Address is the optional address of the Consul server. The port, if any |
|
// will be removed from here and this will be set to the ServerName of the |
|
// resulting config. |
|
Address string |
|
|
|
// CAFile is the optional path to the CA certificate used for Consul |
|
// communication, defaults to the system bundle if not specified. |
|
CAFile string |
|
|
|
// CAPath is the optional path to a directory of CA certificates to use for |
|
// Consul communication, defaults to the system bundle if not specified. |
|
CAPath string |
|
|
|
// CertFile is the optional path to the certificate for Consul |
|
// communication. If this is set then you need to also set KeyFile. |
|
CertFile string |
|
|
|
// KeyFile is the optional path to the private key for Consul communication. |
|
// If this is set then you need to also set CertFile. |
|
KeyFile string |
|
|
|
// InsecureSkipVerify if set to true will disable TLS host verification. |
|
InsecureSkipVerify bool |
|
} |
|
|
|
// DefaultConfig returns a default configuration for the client. By default this |
|
// will pool and reuse idle connections to Consul. If you have a long-lived |
|
// client object, this is the desired behavior and should make the most efficient |
|
// use of the connections to Consul. If you don't reuse a client object , which |
|
// is not recommended, then you may notice idle connections building up over |
|
// time. To avoid this, use the DefaultNonPooledConfig() instead. |
|
func DefaultConfig() *Config { |
|
return defaultConfig(cleanhttp.DefaultPooledTransport) |
|
} |
|
|
|
// DefaultNonPooledConfig returns a default configuration for the client which |
|
// does not pool connections. This isn't a recommended configuration because it |
|
// will reconnect to Consul on every request, but this is useful to avoid the |
|
// accumulation of idle connections if you make many client objects during the |
|
// lifetime of your application. |
|
func DefaultNonPooledConfig() *Config { |
|
return defaultConfig(cleanhttp.DefaultTransport) |
|
} |
|
|
|
// defaultConfig returns the default configuration for the client, using the |
|
// given function to make the transport. |
|
func defaultConfig(transportFn func() *http.Transport) *Config { |
|
config := &Config{ |
|
Address: "127.0.0.1:8500", |
|
Scheme: "http", |
|
Transport: transportFn(), |
|
} |
|
|
|
if addr := os.Getenv(HTTPAddrEnvName); addr != "" { |
|
config.Address = addr |
|
} |
|
|
|
if token := os.Getenv(HTTPTokenEnvName); token != "" { |
|
config.Token = token |
|
} |
|
|
|
if auth := os.Getenv(HTTPAuthEnvName); auth != "" { |
|
var username, password string |
|
if strings.Contains(auth, ":") { |
|
split := strings.SplitN(auth, ":", 2) |
|
username = split[0] |
|
password = split[1] |
|
} else { |
|
username = auth |
|
} |
|
|
|
config.HttpAuth = &HttpBasicAuth{ |
|
Username: username, |
|
Password: password, |
|
} |
|
} |
|
|
|
if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" { |
|
enabled, err := strconv.ParseBool(ssl) |
|
if err != nil { |
|
log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLEnvName, err) |
|
} |
|
|
|
if enabled { |
|
config.Scheme = "https" |
|
} |
|
} |
|
|
|
if v := os.Getenv(HTTPTLSServerName); v != "" { |
|
config.TLSConfig.Address = v |
|
} |
|
if v := os.Getenv(HTTPCAFile); v != "" { |
|
config.TLSConfig.CAFile = v |
|
} |
|
if v := os.Getenv(HTTPCAPath); v != "" { |
|
config.TLSConfig.CAPath = v |
|
} |
|
if v := os.Getenv(HTTPClientCert); v != "" { |
|
config.TLSConfig.CertFile = v |
|
} |
|
if v := os.Getenv(HTTPClientKey); v != "" { |
|
config.TLSConfig.KeyFile = v |
|
} |
|
if v := os.Getenv(HTTPSSLVerifyEnvName); v != "" { |
|
doVerify, err := strconv.ParseBool(v) |
|
if err != nil { |
|
log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLVerifyEnvName, err) |
|
} |
|
if !doVerify { |
|
config.TLSConfig.InsecureSkipVerify = true |
|
} |
|
} |
|
|
|
return config |
|
} |
|
|
|
// TLSConfig is used to generate a TLSClientConfig that's useful for talking to |
|
// Consul using TLS. |
|
func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) { |
|
tlsClientConfig := &tls.Config{ |
|
InsecureSkipVerify: tlsConfig.InsecureSkipVerify, |
|
} |
|
|
|
if tlsConfig.Address != "" { |
|
server := tlsConfig.Address |
|
hasPort := strings.LastIndex(server, ":") > strings.LastIndex(server, "]") |
|
if hasPort { |
|
var err error |
|
server, _, err = net.SplitHostPort(server) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
tlsClientConfig.ServerName = server |
|
} |
|
|
|
if tlsConfig.CertFile != "" && tlsConfig.KeyFile != "" { |
|
tlsCert, err := tls.LoadX509KeyPair(tlsConfig.CertFile, tlsConfig.KeyFile) |
|
if err != nil { |
|
return nil, err |
|
} |
|
tlsClientConfig.Certificates = []tls.Certificate{tlsCert} |
|
} |
|
|
|
rootConfig := &rootcerts.Config{ |
|
CAFile: tlsConfig.CAFile, |
|
CAPath: tlsConfig.CAPath, |
|
} |
|
if err := rootcerts.ConfigureTLS(tlsClientConfig, rootConfig); err != nil { |
|
return nil, err |
|
} |
|
|
|
return tlsClientConfig, nil |
|
} |
|
|
|
// Client provides a client to the Consul API |
|
type Client struct { |
|
config Config |
|
} |
|
|
|
// NewClient returns a new client |
|
func NewClient(config *Config) (*Client, error) { |
|
// bootstrap the config |
|
defConfig := DefaultConfig() |
|
|
|
if len(config.Address) == 0 { |
|
config.Address = defConfig.Address |
|
} |
|
|
|
if len(config.Scheme) == 0 { |
|
config.Scheme = defConfig.Scheme |
|
} |
|
|
|
if config.Transport == nil { |
|
config.Transport = defConfig.Transport |
|
} |
|
|
|
if config.HttpClient == nil { |
|
config.HttpClient = defConfig.HttpClient |
|
} |
|
|
|
if config.TLSConfig.Address == "" { |
|
config.TLSConfig.Address = defConfig.TLSConfig.Address |
|
} |
|
|
|
if config.TLSConfig.CAFile == "" { |
|
config.TLSConfig.CAFile = defConfig.TLSConfig.CAFile |
|
} |
|
|
|
if config.TLSConfig.CAPath == "" { |
|
config.TLSConfig.CAPath = defConfig.TLSConfig.CAPath |
|
} |
|
|
|
if config.TLSConfig.CertFile == "" { |
|
config.TLSConfig.CertFile = defConfig.TLSConfig.CertFile |
|
} |
|
|
|
if config.TLSConfig.KeyFile == "" { |
|
config.TLSConfig.KeyFile = defConfig.TLSConfig.KeyFile |
|
} |
|
|
|
if !config.TLSConfig.InsecureSkipVerify { |
|
config.TLSConfig.InsecureSkipVerify = defConfig.TLSConfig.InsecureSkipVerify |
|
} |
|
|
|
if config.HttpClient == nil { |
|
var err error |
|
config.HttpClient, err = NewHttpClient(config.Transport, config.TLSConfig) |
|
if err != nil { |
|
return nil, err |
|
} |
|
} |
|
|
|
parts := strings.SplitN(config.Address, "://", 2) |
|
if len(parts) == 2 { |
|
switch parts[0] { |
|
case "http": |
|
case "https": |
|
config.Scheme = "https" |
|
case "unix": |
|
trans := cleanhttp.DefaultTransport() |
|
trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { |
|
return net.Dial("unix", parts[1]) |
|
} |
|
config.HttpClient = &http.Client{ |
|
Transport: trans, |
|
} |
|
default: |
|
return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) |
|
} |
|
config.Address = parts[1] |
|
} |
|
|
|
client := &Client{ |
|
config: *config, |
|
} |
|
return client, nil |
|
} |
|
|
|
// NewHttpClient returns an http client configured with the given Transport and TLS |
|
// config. |
|
func NewHttpClient(transport *http.Transport, tlsConf TLSConfig) (*http.Client, error) { |
|
tlsClientConfig, err := SetupTLSConfig(&tlsConf) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
transport.TLSClientConfig = tlsClientConfig |
|
client := &http.Client{ |
|
Transport: transport, |
|
} |
|
|
|
return client, nil |
|
} |
|
|
|
// request is used to help build up a request |
|
type request struct { |
|
config *Config |
|
method string |
|
url *url.URL |
|
params url.Values |
|
body io.Reader |
|
header http.Header |
|
obj interface{} |
|
} |
|
|
|
// setQueryOptions is used to annotate the request with |
|
// additional query options |
|
func (r *request) setQueryOptions(q *QueryOptions) { |
|
if q == nil { |
|
return |
|
} |
|
if q.Datacenter != "" { |
|
r.params.Set("dc", q.Datacenter) |
|
} |
|
if q.AllowStale { |
|
r.params.Set("stale", "") |
|
} |
|
if q.RequireConsistent { |
|
r.params.Set("consistent", "") |
|
} |
|
if q.WaitIndex != 0 { |
|
r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10)) |
|
} |
|
if q.WaitTime != 0 { |
|
r.params.Set("wait", durToMsec(q.WaitTime)) |
|
} |
|
if q.Token != "" { |
|
r.header.Set("X-Consul-Token", q.Token) |
|
} |
|
if q.Near != "" { |
|
r.params.Set("near", q.Near) |
|
} |
|
if len(q.NodeMeta) > 0 { |
|
for key, value := range q.NodeMeta { |
|
r.params.Add("node-meta", key+":"+value) |
|
} |
|
} |
|
if q.RelayFactor != 0 { |
|
r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor))) |
|
} |
|
} |
|
|
|
// durToMsec converts a duration to a millisecond specified string. If the |
|
// user selected a positive value that rounds to 0 ms, then we will use 1 ms |
|
// so they get a short delay, otherwise Consul will translate the 0 ms into |
|
// a huge default delay. |
|
func durToMsec(dur time.Duration) string { |
|
ms := dur / time.Millisecond |
|
if dur > 0 && ms == 0 { |
|
ms = 1 |
|
} |
|
return fmt.Sprintf("%dms", ms) |
|
} |
|
|
|
// serverError is a string we look for to detect 500 errors. |
|
const serverError = "Unexpected response code: 500" |
|
|
|
// IsServerError returns true for 500 errors from the Consul servers, these are |
|
// usually retryable at a later time. |
|
func IsServerError(err error) bool { |
|
if err == nil { |
|
return false |
|
} |
|
|
|
// TODO (slackpad) - Make a real error type here instead of using |
|
// a string check. |
|
return strings.Contains(err.Error(), serverError) |
|
} |
|
|
|
// setWriteOptions is used to annotate the request with |
|
// additional write options |
|
func (r *request) setWriteOptions(q *WriteOptions) { |
|
if q == nil { |
|
return |
|
} |
|
if q.Datacenter != "" { |
|
r.params.Set("dc", q.Datacenter) |
|
} |
|
if q.Token != "" { |
|
r.header.Set("X-Consul-Token", q.Token) |
|
} |
|
if q.RelayFactor != 0 { |
|
r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor))) |
|
} |
|
} |
|
|
|
// toHTTP converts the request to an HTTP request |
|
func (r *request) toHTTP() (*http.Request, error) { |
|
// Encode the query parameters |
|
r.url.RawQuery = r.params.Encode() |
|
|
|
// Check if we should encode the body |
|
if r.body == nil && r.obj != nil { |
|
b, err := encodeBody(r.obj) |
|
if err != nil { |
|
return nil, err |
|
} |
|
r.body = b |
|
} |
|
|
|
// Create the HTTP request |
|
req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
req.URL.Host = r.url.Host |
|
req.URL.Scheme = r.url.Scheme |
|
req.Host = r.url.Host |
|
req.Header = r.header |
|
|
|
// Setup auth |
|
if r.config.HttpAuth != nil { |
|
req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password) |
|
} |
|
|
|
return req, nil |
|
} |
|
|
|
// newRequest is used to create a new request |
|
func (c *Client) newRequest(method, path string) *request { |
|
r := &request{ |
|
config: &c.config, |
|
method: method, |
|
url: &url.URL{ |
|
Scheme: c.config.Scheme, |
|
Host: c.config.Address, |
|
Path: path, |
|
}, |
|
params: make(map[string][]string), |
|
header: make(http.Header), |
|
} |
|
if c.config.Datacenter != "" { |
|
r.params.Set("dc", c.config.Datacenter) |
|
} |
|
if c.config.WaitTime != 0 { |
|
r.params.Set("wait", durToMsec(r.config.WaitTime)) |
|
} |
|
if c.config.Token != "" { |
|
r.header.Set("X-Consul-Token", r.config.Token) |
|
} |
|
return r |
|
} |
|
|
|
// doRequest runs a request with our client |
|
func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) { |
|
req, err := r.toHTTP() |
|
if err != nil { |
|
return 0, nil, err |
|
} |
|
start := time.Now() |
|
resp, err := c.config.HttpClient.Do(req) |
|
diff := time.Now().Sub(start) |
|
return diff, resp, err |
|
} |
|
|
|
// Query is used to do a GET request against an endpoint |
|
// and deserialize the response into an interface using |
|
// standard Consul conventions. |
|
func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) { |
|
r := c.newRequest("GET", endpoint) |
|
r.setQueryOptions(q) |
|
rtt, resp, err := requireOK(c.doRequest(r)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer resp.Body.Close() |
|
|
|
qm := &QueryMeta{} |
|
parseQueryMeta(resp, qm) |
|
qm.RequestTime = rtt |
|
|
|
if err := decodeBody(resp, out); err != nil { |
|
return nil, err |
|
} |
|
return qm, nil |
|
} |
|
|
|
// write is used to do a PUT request against an endpoint |
|
// and serialize/deserialized using the standard Consul conventions. |
|
func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) { |
|
r := c.newRequest("PUT", endpoint) |
|
r.setWriteOptions(q) |
|
r.obj = in |
|
rtt, resp, err := requireOK(c.doRequest(r)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer resp.Body.Close() |
|
|
|
wm := &WriteMeta{RequestTime: rtt} |
|
if out != nil { |
|
if err := decodeBody(resp, &out); err != nil { |
|
return nil, err |
|
} |
|
} |
|
return wm, nil |
|
} |
|
|
|
// parseQueryMeta is used to help parse query meta-data |
|
func parseQueryMeta(resp *http.Response, q *QueryMeta) error { |
|
header := resp.Header |
|
|
|
// Parse the X-Consul-Index |
|
index, err := strconv.ParseUint(header.Get("X-Consul-Index"), 10, 64) |
|
if err != nil { |
|
return fmt.Errorf("Failed to parse X-Consul-Index: %v", err) |
|
} |
|
q.LastIndex = index |
|
|
|
// Parse the X-Consul-LastContact |
|
last, err := strconv.ParseUint(header.Get("X-Consul-LastContact"), 10, 64) |
|
if err != nil { |
|
return fmt.Errorf("Failed to parse X-Consul-LastContact: %v", err) |
|
} |
|
q.LastContact = time.Duration(last) * time.Millisecond |
|
|
|
// Parse the X-Consul-KnownLeader |
|
switch header.Get("X-Consul-KnownLeader") { |
|
case "true": |
|
q.KnownLeader = true |
|
default: |
|
q.KnownLeader = false |
|
} |
|
|
|
// Parse X-Consul-Translate-Addresses |
|
switch header.Get("X-Consul-Translate-Addresses") { |
|
case "true": |
|
q.AddressTranslationEnabled = true |
|
default: |
|
q.AddressTranslationEnabled = false |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// decodeBody is used to JSON decode a body |
|
func decodeBody(resp *http.Response, out interface{}) error { |
|
dec := json.NewDecoder(resp.Body) |
|
return dec.Decode(out) |
|
} |
|
|
|
// encodeBody is used to encode a request body |
|
func encodeBody(obj interface{}) (io.Reader, error) { |
|
buf := bytes.NewBuffer(nil) |
|
enc := json.NewEncoder(buf) |
|
if err := enc.Encode(obj); err != nil { |
|
return nil, err |
|
} |
|
return buf, nil |
|
} |
|
|
|
// requireOK is used to wrap doRequest and check for a 200 |
|
func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) { |
|
if e != nil { |
|
if resp != nil { |
|
resp.Body.Close() |
|
} |
|
return d, nil, e |
|
} |
|
if resp.StatusCode != 200 { |
|
var buf bytes.Buffer |
|
io.Copy(&buf, resp.Body) |
|
resp.Body.Close() |
|
return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes()) |
|
} |
|
return d, resp, nil |
|
}
|
|
|