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 }