Skip to content

Commit 3df7cc0

Browse files
authored
Adding new helper functions to internal.HTTPClient API (#269)
* Adding experimental OP client * Added unit tests * Made the client more generic * Cleaned up some error checks * Merged JSONHTTPClient into HTTPClient * Moved test case * Further simplified the helper functions * Added CreateErrFn and SuccessFn types * Support for retrying on read errors * Reverted the changes made to auth/ files * Updated documentation * Using CreateErrFn type in Request
1 parent 79d5dcd commit 3df7cc0

File tree

3 files changed

+608
-40
lines changed

3 files changed

+608
-40
lines changed

internal/http_client.go

Lines changed: 146 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,18 @@ import (
4141
type HTTPClient struct {
4242
Client *http.Client
4343
RetryConfig *RetryConfig
44-
ErrParser ErrorParser
44+
ErrParser ErrorParser // Deprecated. Use CreateErrFn instead.
45+
CreateErrFn CreateErrFn
46+
SuccessFn SuccessFn
47+
Opts []HTTPOption
4548
}
4649

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+
4756
// NewHTTPClient creates a new HTTPClient using the provided client options and the default
4857
// RetryConfig.
4958
//
@@ -58,6 +67,7 @@ func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClien
5867
if err != nil {
5968
return nil, "", err
6069
}
70+
6171
twoMinutes := time.Duration(2) * time.Minute
6272
client := &HTTPClient{
6373
Client: hc,
@@ -74,9 +84,32 @@ func NewHTTPClient(ctx context.Context, opts ...option.ClientOption) (*HTTPClien
7484
return client, endpoint, nil
7585
}
7686

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+
77105
// Do executes the given Request, and returns a Response.
78106
//
79107
// 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.
80113
func (c *HTTPClient) Do(ctx context.Context, req *Request) (*Response, error) {
81114
var result *attemptResult
82115
var err error
@@ -93,42 +126,103 @@ func (c *HTTPClient) Do(ctx context.Context, req *Request) (*Response, error) {
93126
return nil, err
94127
}
95128
}
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
97151
}
98152

99153
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)
101155
if err != nil {
102156
return nil, err
103157
}
104158

105159
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
110169
}
111170

112171
// If a RetryConfig is available, always consult it to determine if the request should be retried
113172
// or not. Even if there was a network error, we may not want to retry the request based on the
114173
// RetryConfig that is in effect.
115174
if c.RetryConfig != nil {
116-
delay, retry := c.RetryConfig.retryDelay(retries, resp, err)
175+
delay, retry := c.RetryConfig.retryDelay(retries, resp, result.Err)
117176
result.RetryAfter = delay
118177
result.Retry = retry
119-
if retry && resp != nil {
120-
defer resp.Body.Close()
121-
}
122178
}
123179
return result, nil
124180
}
125181

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+
126221
type attemptResult struct {
127-
Resp *http.Response
222+
Resp *Response
128223
Err error
129224
Retry bool
130225
RetryAfter time.Duration
131-
ErrParser ErrorParser
132226
}
133227

134228
func (r *attemptResult) waitForRetry(ctx context.Context) error {
@@ -141,23 +235,7 @@ func (r *attemptResult) waitForRetry(ctx context.Context) error {
141235
return ctx.Err()
142236
}
143237

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) {
161239
var data io.Reader
162240
if r.Body != nil {
163241
b, err := r.Body.Bytes()
@@ -203,14 +281,6 @@ func (e *jsonEntity) Mime() string {
203281
return "application/json"
204282
}
205283

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-
214284
func newResponse(resp *http.Response, errParser ErrorParser) (*Response, error) {
215285
defer resp.Body.Close()
216286
b, err := ioutil.ReadAll(resp.Body)
@@ -229,6 +299,8 @@ func newResponse(resp *http.Response, errParser ErrorParser) (*Response, error)
229299
//
230300
// Returns an error if the status code does not match. If an ErrorParser is specified, uses that to
231301
// 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.
232304
func (r *Response) CheckStatus(want int) error {
233305
if r.Status == want {
234306
return nil
@@ -249,6 +321,8 @@ func (r *Response) CheckStatus(want int) error {
249321
//
250322
// Unmarshal uses https://golang.org/pkg/encoding/json/#Unmarshal internally, and hence v has the
251323
// same requirements as the json package.
324+
//
325+
// Deprecated. Use DoAndUnmarshal function instead.
252326
func (r *Response) Unmarshal(want int, v interface{}) error {
253327
if err := r.CheckStatus(want); err != nil {
254328
return err
@@ -257,6 +331,8 @@ func (r *Response) Unmarshal(want int, v interface{}) error {
257331
}
258332

259333
// ErrorParser is a function that is used to construct custom error messages.
334+
//
335+
// Deprecated. Use SuccessFn and CreateErrFn instead.
260336
type ErrorParser func([]byte) string
261337

262338
// 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 {
290366
}
291367
}
292368

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+
293401
// RetryConfig specifies how the HTTPClient should retry failing HTTP requests.
294402
//
295403
// A request is never retried more than MaxRetries times. If CheckForRetry is nil, all network

0 commit comments

Comments
 (0)