@@ -41,9 +41,18 @@ import (
41
41
type HTTPClient struct {
42
42
Client * http.Client
43
43
RetryConfig * RetryConfig
44
- ErrParser ErrorParser
44
+ ErrParser ErrorParser // Deprecated. Use CreateErrFn instead.
45
+ CreateErrFn CreateErrFn
46
+ SuccessFn SuccessFn
47
+ Opts []HTTPOption
45
48
}
46
49
50
+ // SuccessFn is a function that checks if a Response indicates success.
51
+ type SuccessFn func (r * Response ) bool
52
+
53
+ // CreateErrFn is a function that creates an error from a given Response.
54
+ type CreateErrFn func (r * Response ) error
55
+
47
56
// NewHTTPClient creates a new HTTPClient using the provided client options and the default
48
57
// RetryConfig.
49
58
//
@@ -58,6 +67,7 @@ func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClien
58
67
if err != nil {
59
68
return nil , "" , err
60
69
}
70
+
61
71
twoMinutes := time .Duration (2 ) * time .Minute
62
72
client := & HTTPClient {
63
73
Client : hc ,
@@ -74,9 +84,32 @@ func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClien
74
84
return client , endpoint , nil
75
85
}
76
86
87
+ // Request contains all the parameters required to construct an outgoing HTTP request.
88
+ type Request struct {
89
+ Method string
90
+ URL string
91
+ Body HTTPEntity
92
+ Opts []HTTPOption
93
+ SuccessFn SuccessFn
94
+ CreateErrFn CreateErrFn
95
+ }
96
+
97
+ // Response contains information extracted from an HTTP response.
98
+ type Response struct {
99
+ Status int
100
+ Header http.Header
101
+ Body []byte
102
+ errParser ErrorParser
103
+ }
104
+
77
105
// Do executes the given Request, and returns a Response.
78
106
//
79
107
// If a RetryConfig is specified on the client, Do attempts to retry failing requests.
108
+ //
109
+ // If SuccessFn is set on the client or on the request, the response is validated against that
110
+ // function. If this validation fails, returns an error. These errors are created using the
111
+ // CreateErrFn on the client or on the request. If neither is set, CreatePlatformError is
112
+ // used as the default error function.
80
113
func (c * HTTPClient ) Do (ctx context.Context , req * Request ) (* Response , error ) {
81
114
var result * attemptResult
82
115
var err error
@@ -93,42 +126,103 @@ func (c *HTTPClient) Do(ctx context.Context, req *Request) (*Response, error) {
93
126
return nil , err
94
127
}
95
128
}
96
- return result .handleResponse ()
129
+ return c .handleResult (req , result )
130
+ }
131
+
132
+ // DoAndUnmarshal behaves similar to Do, but additionally unmarshals the response payload into
133
+ // the given pointer.
134
+ //
135
+ // Unmarshal takes place only if the response does not represent an error (as determined by
136
+ // the Do function) and v is not nil. If the unmarshal fails, an error is returned even if the
137
+ // original response indicated success.
138
+ func (c * HTTPClient ) DoAndUnmarshal (ctx context.Context , req * Request , v interface {}) (* Response , error ) {
139
+ resp , err := c .Do (ctx , req )
140
+ if err != nil {
141
+ return nil , err
142
+ }
143
+
144
+ if v != nil {
145
+ if err := json .Unmarshal (resp .Body , v ); err != nil {
146
+ return nil , fmt .Errorf ("error while parsing response: %v" , err )
147
+ }
148
+ }
149
+
150
+ return resp , nil
97
151
}
98
152
99
153
func (c * HTTPClient ) attempt (ctx context.Context , req * Request , retries int ) (* attemptResult , error ) {
100
- hr , err := req .buildHTTPRequest ()
154
+ hr , err := req .buildHTTPRequest (c . Opts )
101
155
if err != nil {
102
156
return nil , err
103
157
}
104
158
105
159
resp , err := c .Client .Do (hr .WithContext (ctx ))
106
- result := & attemptResult {
107
- Resp : resp ,
108
- Err : err ,
109
- ErrParser : c .ErrParser ,
160
+ result := & attemptResult {}
161
+ if err != nil {
162
+ result .Err = err
163
+ } else {
164
+ // Read the response body here forcing any I/O errors to occur so that retry logic will
165
+ // cover them as well.
166
+ ir , err := newResponse (resp , c .ErrParser )
167
+ result .Resp = ir
168
+ result .Err = err
110
169
}
111
170
112
171
// If a RetryConfig is available, always consult it to determine if the request should be retried
113
172
// or not. Even if there was a network error, we may not want to retry the request based on the
114
173
// RetryConfig that is in effect.
115
174
if c .RetryConfig != nil {
116
- delay , retry := c .RetryConfig .retryDelay (retries , resp , err )
175
+ delay , retry := c .RetryConfig .retryDelay (retries , resp , result . Err )
117
176
result .RetryAfter = delay
118
177
result .Retry = retry
119
- if retry && resp != nil {
120
- defer resp .Body .Close ()
121
- }
122
178
}
123
179
return result , nil
124
180
}
125
181
182
+ func (c * HTTPClient ) handleResult (req * Request , result * attemptResult ) (* Response , error ) {
183
+ if result .Err != nil {
184
+ return nil , fmt .Errorf ("error while making http call: %v" , result .Err )
185
+ }
186
+
187
+ if ! c .success (req , result .Resp ) {
188
+ return nil , c .newError (req , result .Resp )
189
+ }
190
+
191
+ return result .Resp , nil
192
+ }
193
+
194
+ func (c * HTTPClient ) success (req * Request , resp * Response ) bool {
195
+ var successFn SuccessFn
196
+ if req .SuccessFn != nil {
197
+ successFn = req .SuccessFn
198
+ } else if c .SuccessFn != nil {
199
+ successFn = c .SuccessFn
200
+ }
201
+
202
+ if successFn != nil {
203
+ return successFn (resp )
204
+ }
205
+
206
+ // TODO: Default to HasSuccessStatusCode
207
+ return true
208
+ }
209
+
210
+ func (c * HTTPClient ) newError (req * Request , resp * Response ) error {
211
+ createErr := CreatePlatformError
212
+ if req .CreateErrFn != nil {
213
+ createErr = req .CreateErrFn
214
+ } else if c .CreateErrFn != nil {
215
+ createErr = c .CreateErrFn
216
+ }
217
+
218
+ return createErr (resp )
219
+ }
220
+
126
221
type attemptResult struct {
127
- Resp * http. Response
222
+ Resp * Response
128
223
Err error
129
224
Retry bool
130
225
RetryAfter time.Duration
131
- ErrParser ErrorParser
132
226
}
133
227
134
228
func (r * attemptResult ) waitForRetry (ctx context.Context ) error {
@@ -141,23 +235,7 @@ func (r *attemptResult) waitForRetry(ctx context.Context) error {
141
235
return ctx .Err ()
142
236
}
143
237
144
- func (r * attemptResult ) handleResponse () (* Response , error ) {
145
- if r .Err != nil {
146
- return nil , r .Err
147
- }
148
- return newResponse (r .Resp , r .ErrParser )
149
- }
150
-
151
- // Request contains all the parameters required to construct an outgoing HTTP request.
152
- type Request struct {
153
- Method string
154
- URL string
155
- Body HTTPEntity
156
- Opts []HTTPOption
157
- }
158
-
159
- func (r * Request ) buildHTTPRequest () (* http.Request , error ) {
160
- var opts []HTTPOption
238
+ func (r * Request ) buildHTTPRequest (opts []HTTPOption ) (* http.Request , error ) {
161
239
var data io.Reader
162
240
if r .Body != nil {
163
241
b , err := r .Body .Bytes ()
@@ -203,14 +281,6 @@ func (e *jsonEntity) Mime() string {
203
281
return "application/json"
204
282
}
205
283
206
- // Response contains information extracted from an HTTP response.
207
- type Response struct {
208
- Status int
209
- Header http.Header
210
- Body []byte
211
- errParser ErrorParser
212
- }
213
-
214
284
func newResponse (resp * http.Response , errParser ErrorParser ) (* Response , error ) {
215
285
defer resp .Body .Close ()
216
286
b , err := ioutil .ReadAll (resp .Body )
@@ -229,6 +299,8 @@ func newResponse(resp *http.Response, errParser ErrorParser) (*Response, error)
229
299
//
230
300
// Returns an error if the status code does not match. If an ErrorParser is specified, uses that to
231
301
// construct the returned error message. Otherwise includes the full response body in the error.
302
+ //
303
+ // Deprecated. Directly verify the Status field on the Response instead.
232
304
func (r * Response ) CheckStatus (want int ) error {
233
305
if r .Status == want {
234
306
return nil
@@ -249,6 +321,8 @@ func (r *Response) CheckStatus(want int) error {
249
321
//
250
322
// Unmarshal uses https://golang.org/pkg/encoding/json/#Unmarshal internally, and hence v has the
251
323
// same requirements as the json package.
324
+ //
325
+ // Deprecated. Use DoAndUnmarshal function instead.
252
326
func (r * Response ) Unmarshal (want int , v interface {}) error {
253
327
if err := r .CheckStatus (want ); err != nil {
254
328
return err
@@ -257,6 +331,8 @@ func (r *Response) Unmarshal(want int, v interface{}) error {
257
331
}
258
332
259
333
// ErrorParser is a function that is used to construct custom error messages.
334
+ //
335
+ // Deprecated. Use SuccessFn and CreateErrFn instead.
260
336
type ErrorParser func ([]byte ) string
261
337
262
338
// HTTPOption is an additional parameter that can be specified to customize an outgoing request.
@@ -290,6 +366,38 @@ func WithQueryParams(qp map[string]string) HTTPOption {
290
366
}
291
367
}
292
368
369
+ // HasSuccessStatus returns true if the response status code is in the 2xx range.
370
+ func HasSuccessStatus (r * Response ) bool {
371
+ return r .Status >= http .StatusOK && r .Status < http .StatusNotModified
372
+ }
373
+
374
+ // CreatePlatformError parses the response payload as a GCP error response
375
+ // and create an error from the details extracted.
376
+ //
377
+ // If the response failes to parse, or otherwise doesn't provide any useful details
378
+ // CreatePlatformError creates an error with some sensible defaults.
379
+ func CreatePlatformError (resp * Response ) error {
380
+ var gcpError struct {
381
+ Error struct {
382
+ Status string `json:"status"`
383
+ Message string `json:"message"`
384
+ } `json:"error"`
385
+ }
386
+ json .Unmarshal (resp .Body , & gcpError ) // ignore any json parse errors at this level
387
+ code := gcpError .Error .Status
388
+ if code == "" {
389
+ code = "UNKNOWN"
390
+ }
391
+
392
+ message := gcpError .Error .Message
393
+ if message == "" {
394
+ message = fmt .Sprintf (
395
+ "unexpected http response with status: %d; body: %s" , resp .Status , string (resp .Body ))
396
+ }
397
+
398
+ return Error (code , message )
399
+ }
400
+
293
401
// RetryConfig specifies how the HTTPClient should retry failing HTTP requests.
294
402
//
295
403
// A request is never retried more than MaxRetries times. If CheckForRetry is nil, all network
0 commit comments