This repository has been archived by the owner on Mar 12, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
/
client.go
305 lines (265 loc) · 8.62 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
package clientlib
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/docker/licensing/lib/errors"
)
// Do is a shortcut for creating and executing an http request.
func Do(ctx context.Context, method, urlStr string, opts ...RequestOption) (*http.Request, *http.Response, error) {
r, err := New(ctx, method, urlStr, opts...)
if err != nil {
return nil, nil, err
}
res, err := r.Do()
return r.Request, res, err
}
// New creates and returns a new Request, potentially configured via a vector of RequestOption's.
func New(ctx context.Context, method, urlStr string, opts ...RequestOption) (*Request, error) {
req, err := http.NewRequest(method, urlStr, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
r := &Request{
Request: req,
Client: &http.Client{},
ErrorCheck: DefaultErrorCheck,
ErrorBodyMaxLength: defaultErrBodyMaxLength,
ErrorSummary: DefaultErrorSummary,
RequestPrepare: DefaultRequestPrepare,
ResponseHandle: DefaultResponseHandle,
}
for _, o := range opts {
o(r)
}
return r, nil
}
// Request encompasses an http.Request, plus configured behavior options.
type Request struct {
*http.Request
Client *http.Client
ErrorCheck ErrorCheck
ErrorBodyMaxLength int64
ErrorSummary ErrorSummary
ResponseHandle ResponseHandle
RequestPrepare RequestPrepare
}
// Do executes the Request. The Request.ErrorCheck to determine
// if this attempt has failed, and transform the returned error.
// Otherwise, Request.ResponseHandler will examine the response.
// It's expected that the ResponseHandler has been configured via
// a RequestOption to perform response parsing and storing.
func (r *Request) Do() (*http.Response, error) {
err := r.RequestPrepare(r)
if err != nil {
return nil, err
}
res, err := r.Client.Do(r.Request)
err = r.ErrorCheck(r, err, res)
if err != nil {
return res, err
}
return res, r.ResponseHandle(r, res)
}
// SetBody mirrors the ReadCloser config in http.NewRequest,
// ensuring that a ReadCloser is used for http.Request.Body.
func (r *Request) SetBody(body io.Reader) {
rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = ioutil.NopCloser(body)
}
r.Body = rc
}
// ErrorFields returns error annotation fields for the request.
func (r *Request) ErrorFields() map[string]interface{} {
return map[string]interface{}{
"url": r.URL.String(),
"method": r.Method,
}
}
// ErrorCheck is the signature for the function that is passed
// the error & response immediately from http.Client.Do().
type ErrorCheck func(r *Request, doErr error, res *http.Response) error
// DefaultErrorCheck is the default error checker used if none is
// configured on a Request. doErr and res are the return values of
// executing http.Client.Do(), so any implementation should first
// check doErr for non-nill & react appropriately. If an http response
// was received, if a non-200 class status was also received, then
// the response body will be read (up to a const limit) and passed
// to request.ErrorSummary to attempt to parse out the error body,
// which will be passed as the "detail" flag on the returned error.
func DefaultErrorCheck(r *Request, doErr error, res *http.Response) error {
if doErr != nil {
return errors.Wrap(doErr, r.ErrorFields())
}
status := res.StatusCode
if status >= 200 && status < 300 {
return nil
}
defer res.Body.Close()
body, _ := ioutil.ReadAll(io.LimitReader(res.Body, r.ErrorBodyMaxLength))
detail := r.ErrorSummary(body)
message := fmt.Sprintf("%s %s returned %d : %s", r.Method, r.URL.String(), status, detail)
return errors.NewHTTPError(status, message).
With(r.ErrorFields()).
With(map[string]interface{}{
"http_status": status,
"detail": detail,
})
}
// Default error response max length, in bytes
const defaultErrBodyMaxLength = 256
// ErrorSummary is the signature for the function that is passed
// the fully read body of an error response.
type ErrorSummary func([]byte) string
// DefaultErrorSummary just returns the string of the received
// error body. Note that the body passed in is potentially truncated
// before this call.
func DefaultErrorSummary(body []byte) string {
return string(body)
}
// RequestPrepare is the signature for the function called
// before calling http.Client.Do, to perform any preparation
// needed before executing the request, eg. marshaling the body.
type RequestPrepare func(r *Request) error
// DefaultRequestPrepare does nothing.
func DefaultRequestPrepare(*Request) error {
return nil
}
// ResponseHandle is the signature for the function called if
// ErrorCheck returns a nil error, and is responsible for performing
// any reads or stores from the request & response.
type ResponseHandle func(*Request, *http.Response) error
// DefaultResponseHandle merely closes the response body.
func DefaultResponseHandle(r *Request, res *http.Response) error {
res.Body.Close()
return nil
}
// RequestOption is the signature for functions that can perform
// some configuration of a Request.
type RequestOption func(*Request)
// SendJSON returns a RequestOption that will marshal
// and set the json body & headers on a request.
func SendJSON(sends interface{}) RequestOption {
return func(r *Request) {
r.Header.Set("Content-Type", "application/json")
r.RequestPrepare = func(r *Request) error {
bits, err := json.Marshal(sends)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
body := bytes.NewReader(bits)
r.SetBody(body)
r.ContentLength = int64(body.Len())
return nil
}
}
}
// RecvJSON returns a RequestOption that will set the json headers
// on a request, and set a ResponseHandler that will unmarshal
// the response body to the given interface{}.
func RecvJSON(recvs interface{}) RequestOption {
return func(r *Request) {
r.Header.Set("Accept", "application/json")
r.Header.Set("Accept-Charset", "utf-8")
r.ResponseHandle = func(r *Request, res *http.Response) error {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
err = json.Unmarshal(body, recvs)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
return nil
}
}
}
// SendXML returns a RequestOption that will marshal
// and set the xml body & headers on a request.
func SendXML(sends interface{}) RequestOption {
return func(r *Request) {
r.Header.Set("Content-Type", "application/xml")
r.RequestPrepare = func(r *Request) error {
bits, err := xml.Marshal(sends)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
body := bytes.NewReader(bits)
r.SetBody(body)
r.ContentLength = int64(body.Len())
return nil
}
}
}
// RecvXML returns a RequestOption that will set the xml headers
// on a request, and set a ResponseHandler that will unmarshal
// the response body to the given interface{}.
func RecvXML(recvs interface{}) RequestOption {
return func(r *Request) {
r.Header.Set("Accept", "application/xml")
r.Header.Set("Accept-Charset", "utf-8")
r.ResponseHandle = func(r *Request, res *http.Response) error {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
err = xml.Unmarshal(body, recvs)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
return nil
}
}
}
// SendText returns a RequestOption that will marshal
// and set the text body & headers on a request.
func SendText(sends string) RequestOption {
return func(r *Request) {
r.Header.Set("Content-Type", "text/plain")
r.RequestPrepare = func(r *Request) error {
body := strings.NewReader(sends)
r.SetBody(body)
r.ContentLength = int64(body.Len())
return nil
}
}
}
// RecvText returns a RequestOption that will set the text headers
// on a request, and set a ResponseHandler that will unmarshal
// the response body to the given string.
func RecvText(recvs *string) RequestOption {
return func(r *Request) {
r.Header.Set("Accept", "text/plain")
r.Header.Set("Accept-Charset", "utf-8")
r.ResponseHandle = func(r *Request, res *http.Response) error {
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return errors.Wrap(err, r.ErrorFields())
}
sbody := string(body)
*recvs = sbody
return nil
}
}
}
// DontClose sets the ResponseBody to an empty function, so that the
// response body is not automatically closed. Users of this should be
// sure to call res.Body.Close().
func DontClose() RequestOption {
return func(r *Request) {
r.ResponseHandle = func(*Request, *http.Response) error {
return nil
}
}
}