github.com/zak-blake/goa@v1.4.1/client/client.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/base64"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/http/httputil"
    11  	"time"
    12  
    13  	"context"
    14  
    15  	"github.com/goadesign/goa"
    16  )
    17  
    18  type (
    19  	// Doer defines the Do method of the http client.
    20  	Doer interface {
    21  		Do(context.Context, *http.Request) (*http.Response, error)
    22  	}
    23  
    24  	// Client is the common client data structure for all goa service clients.
    25  	Client struct {
    26  		// Doer is the underlying http client.
    27  		Doer
    28  		// Scheme overrides the default action scheme.
    29  		Scheme string
    30  		// Host is the service hostname.
    31  		Host string
    32  		// UserAgent is the user agent set in requests made by the client.
    33  		UserAgent string
    34  		// Dump indicates whether to dump request response.
    35  		Dump bool
    36  	}
    37  )
    38  
    39  // New creates a new API client that wraps c.
    40  // If c is nil, the returned client wraps http.DefaultClient.
    41  func New(c Doer) *Client {
    42  	if c == nil {
    43  		c = HTTPClientDoer(http.DefaultClient)
    44  	}
    45  	return &Client{Doer: c}
    46  }
    47  
    48  // HTTPClientDoer turns a stdlib http.Client into a Doer. Use it to enable to call New() with an http.Client.
    49  func HTTPClientDoer(hc *http.Client) Doer {
    50  	return doFunc(func(_ context.Context, req *http.Request) (*http.Response, error) {
    51  		return hc.Do(req)
    52  	})
    53  }
    54  
    55  // doFunc is the type definition of the Doer.Do method. It implements Doer.
    56  type doFunc func(context.Context, *http.Request) (*http.Response, error)
    57  
    58  // Do implements Doer.Do
    59  func (f doFunc) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
    60  	return f(ctx, req)
    61  }
    62  
    63  // Do wraps the underlying http client Do method and adds logging.
    64  // The logger should be in the context.
    65  func (c *Client) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
    66  	// TODO: setting the request ID should be done via client middleware. For now only set it if the
    67  	// caller provided one in the ctx.
    68  	if ctxreqid := ContextRequestID(ctx); ctxreqid != "" {
    69  		req.Header.Set("X-Request-Id", ctxreqid)
    70  	}
    71  	if c.UserAgent != "" {
    72  		req.Header.Set("User-Agent", c.UserAgent)
    73  	}
    74  	startedAt := time.Now()
    75  	ctx, id := ContextWithRequestID(ctx)
    76  	goa.LogInfo(ctx, "started", "id", id, req.Method, req.URL.String())
    77  	if c.Dump {
    78  		c.dumpRequest(ctx, req)
    79  	}
    80  	resp, err := c.Doer.Do(ctx, req)
    81  	if err != nil {
    82  		goa.LogError(ctx, "failed", "err", err)
    83  		return nil, err
    84  	}
    85  	goa.LogInfo(ctx, "completed", "id", id, "status", resp.StatusCode, "time", time.Since(startedAt).String())
    86  	if c.Dump {
    87  		c.dumpResponse(ctx, resp)
    88  	}
    89  	return resp, err
    90  }
    91  
    92  // Dump request if needed.
    93  func (c *Client) dumpRequest(ctx context.Context, req *http.Request) {
    94  	reqBody, err := dumpReqBody(req)
    95  	if err != nil {
    96  		goa.LogError(ctx, "Failed to load request body for dump", "err", err.Error())
    97  	}
    98  	goa.LogInfo(ctx, "request headers", headersToSlice(req.Header)...)
    99  	if reqBody != nil {
   100  		goa.LogInfo(ctx, "request", "body", string(reqBody))
   101  	}
   102  }
   103  
   104  // dumpResponse dumps the response and the request.
   105  func (c *Client) dumpResponse(ctx context.Context, resp *http.Response) {
   106  	respBody, _ := dumpRespBody(resp)
   107  	goa.LogInfo(ctx, "response headers", headersToSlice(resp.Header)...)
   108  	if respBody != nil {
   109  		goa.LogInfo(ctx, "response", "body", string(respBody))
   110  	}
   111  }
   112  
   113  // headersToSlice produces a loggable slice from a HTTP header.
   114  func headersToSlice(header http.Header) []interface{} {
   115  	res := make([]interface{}, 2*len(header))
   116  	i := 0
   117  	for k, v := range header {
   118  		res[i] = k
   119  		if len(v) == 1 {
   120  			res[i+1] = v[0]
   121  		} else {
   122  			res[i+1] = v
   123  		}
   124  		i += 2
   125  	}
   126  	return res
   127  }
   128  
   129  // Dump request body, strongly inspired from httputil.DumpRequest
   130  func dumpReqBody(req *http.Request) ([]byte, error) {
   131  	if req.Body == nil {
   132  		return nil, nil
   133  	}
   134  	var save io.ReadCloser
   135  	var err error
   136  	save, req.Body, err = drainBody(req.Body)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	var b bytes.Buffer
   141  	var dest io.Writer = &b
   142  	chunked := len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked"
   143  	if chunked {
   144  		dest = httputil.NewChunkedWriter(dest)
   145  	}
   146  	_, err = io.Copy(dest, req.Body)
   147  	if chunked {
   148  		dest.(io.Closer).Close()
   149  		io.WriteString(&b, "\r\n")
   150  	}
   151  	req.Body = save
   152  	return b.Bytes(), err
   153  }
   154  
   155  // Dump response body, strongly inspired from httputil.DumpResponse
   156  func dumpRespBody(resp *http.Response) ([]byte, error) {
   157  	if resp.Body == nil {
   158  		return nil, nil
   159  	}
   160  	var b bytes.Buffer
   161  	savecl := resp.ContentLength
   162  	var save io.ReadCloser
   163  	var err error
   164  	save, resp.Body, err = drainBody(resp.Body)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	_, err = io.Copy(&b, resp.Body)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	resp.Body = save
   173  	resp.ContentLength = savecl
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	return b.Bytes(), nil
   178  }
   179  
   180  // One of the copies, say from b to r2, could be avoided by using a more
   181  // elaborate trick where the other copy is made during Request/Response.Write.
   182  // This would complicate things too much, given that these functions are for
   183  // debugging only.
   184  func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
   185  	var buf bytes.Buffer
   186  	if _, err = buf.ReadFrom(b); err != nil {
   187  		return nil, nil, err
   188  	}
   189  	if err = b.Close(); err != nil {
   190  		return nil, nil, err
   191  	}
   192  	return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
   193  }
   194  
   195  // headerIterator is a HTTP header iterator.
   196  type headerIterator func(name string, value []string)
   197  
   198  // filterHeaders iterates through the headers skipping hidden headers.
   199  // It calls the given iterator for each header name/value pair. The values are serialized as
   200  // strings.
   201  func filterHeaders(headers http.Header, iterator headerIterator) {
   202  	for k, v := range headers {
   203  		// Skip sensitive headers
   204  		if k == "Authorization" || k == "Cookie" {
   205  			iterator(k, []string{"*****"})
   206  			continue
   207  		}
   208  		iterator(k, v)
   209  	}
   210  }
   211  
   212  // shortID produces a "unique" 6 bytes long string.
   213  // Do not use as a reliable way to get unique IDs, instead use for things like logging.
   214  func shortID() string {
   215  	b := make([]byte, 6)
   216  	io.ReadFull(rand.Reader, b)
   217  	return base64.StdEncoding.EncodeToString(b)
   218  }
   219  
   220  // clientKey is the private type used to store values in the context.
   221  // It is private to avoid possible collisions with keys used by other packages.
   222  type clientKey int
   223  
   224  // ReqIDKey is the context key used to store the request ID value.
   225  const reqIDKey clientKey = 1
   226  
   227  // ContextRequestID extracts the Request ID from the context.
   228  func ContextRequestID(ctx context.Context) string {
   229  	var reqID string
   230  	id := ctx.Value(reqIDKey)
   231  	if id != nil {
   232  		reqID = id.(string)
   233  	}
   234  	return reqID
   235  }
   236  
   237  // ContextWithRequestID returns ctx and the request ID if it already has one or creates and returns a new context with
   238  // a new request ID.
   239  func ContextWithRequestID(ctx context.Context) (context.Context, string) {
   240  	reqID := ContextRequestID(ctx)
   241  	if reqID == "" {
   242  		reqID = shortID()
   243  		ctx = context.WithValue(ctx, reqIDKey, reqID)
   244  	}
   245  	return ctx, reqID
   246  }
   247  
   248  // SetContextRequestID sets a request ID in the given context and returns a new context.
   249  func SetContextRequestID(ctx context.Context, reqID string) context.Context {
   250  	return context.WithValue(ctx, reqIDKey, reqID)
   251  }