Skip to content

Commit 252b3bb

Browse files
authored
Reusable HTTP Client API (#36)
* Implemented a reusable HTTP client API * Added test cases * Comment clean up * Simplified the usage by adding HTTPClient * Using the old ctx import * Support for arbitrary entity types in the request * Renamed fields; Added documentation * Removing a redundant else case * renamed local method
1 parent aed47cf commit 252b3bb

File tree

2 files changed

+458
-0
lines changed

2 files changed

+458
-0
lines changed

internal/http_client.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2017 Google Inc. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"fmt"
21+
"io"
22+
"io/ioutil"
23+
"net/http"
24+
25+
"golang.org/x/net/context"
26+
)
27+
28+
// HTTPClient is a convenient API to make HTTP calls.
29+
//
30+
// This API handles some of the repetitive tasks such as entity serialization and deserialization
31+
// involved in making HTTP calls. It provides a convenient mechanism to set headers and query
32+
// parameters on outgoing requests, while enforcing that an explicit context is used per request.
33+
// Responses returned by HTTPClient can be easily parsed as JSON, and provide a simple mechanism to
34+
// extract error details.
35+
type HTTPClient struct {
36+
Client *http.Client
37+
ErrParser ErrorParser
38+
}
39+
40+
// Do executes the given Request, and returns a Response.
41+
func (c *HTTPClient) Do(ctx context.Context, r *Request) (*Response, error) {
42+
req, err := r.buildHTTPRequest()
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
resp, err := c.Client.Do(req.WithContext(ctx))
48+
if err != nil {
49+
return nil, err
50+
}
51+
defer resp.Body.Close()
52+
53+
b, err := ioutil.ReadAll(resp.Body)
54+
if err != nil {
55+
return nil, err
56+
}
57+
return &Response{
58+
Status: resp.StatusCode,
59+
Body: b,
60+
Header: resp.Header,
61+
errParser: c.ErrParser,
62+
}, nil
63+
}
64+
65+
// Request contains all the parameters required to construct an outgoing HTTP request.
66+
type Request struct {
67+
Method string
68+
URL string
69+
Body HTTPEntity
70+
Opts []HTTPOption
71+
}
72+
73+
func (r *Request) buildHTTPRequest() (*http.Request, error) {
74+
var opts []HTTPOption
75+
var data io.Reader
76+
if r.Body != nil {
77+
b, err := r.Body.Bytes()
78+
if err != nil {
79+
return nil, err
80+
}
81+
data = bytes.NewBuffer(b)
82+
opts = append(opts, WithHeader("Content-Type", r.Body.Mime()))
83+
}
84+
85+
req, err := http.NewRequest(r.Method, r.URL, data)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
opts = append(opts, r.Opts...)
91+
for _, o := range opts {
92+
o(req)
93+
}
94+
return req, nil
95+
}
96+
97+
// HTTPEntity represents a payload that can be included in an outgoing HTTP request.
98+
type HTTPEntity interface {
99+
Bytes() ([]byte, error)
100+
Mime() string
101+
}
102+
103+
type jsonEntity struct {
104+
Val interface{}
105+
}
106+
107+
// NewJSONEntity creates a new HTTPEntity that will be serialized into JSON.
108+
func NewJSONEntity(v interface{}) HTTPEntity {
109+
return &jsonEntity{Val: v}
110+
}
111+
112+
func (e *jsonEntity) Bytes() ([]byte, error) {
113+
return json.Marshal(e.Val)
114+
}
115+
116+
func (e *jsonEntity) Mime() string {
117+
return "application/json"
118+
}
119+
120+
// Response contains information extracted from an HTTP response.
121+
type Response struct {
122+
Status int
123+
Header http.Header
124+
Body []byte
125+
errParser ErrorParser
126+
}
127+
128+
// CheckStatus checks whether the Response status code has the given HTTP status code.
129+
//
130+
// Returns an error if the status code does not match. If an ErroParser is specified, uses that to
131+
// construct the returned error message. Otherwise includes the full response body in the error.
132+
func (r *Response) CheckStatus(want int) error {
133+
if r.Status == want {
134+
return nil
135+
}
136+
137+
var msg string
138+
if r.errParser != nil {
139+
msg = r.errParser(r.Body)
140+
}
141+
if msg == "" {
142+
msg = string(r.Body)
143+
}
144+
return fmt.Errorf("http error status: %d; reason: %s", r.Status, msg)
145+
}
146+
147+
// Unmarshal checks if the Response has the given HTTP status code, and if so unmarshals the
148+
// response body into the variable pointed by v.
149+
//
150+
// Unmarshal uses https://golang.org/pkg/encoding/json/#Unmarshal internally, and hence v has the
151+
// same requirements as the json package.
152+
func (r *Response) Unmarshal(want int, v interface{}) error {
153+
if err := r.CheckStatus(want); err != nil {
154+
return err
155+
}
156+
if err := json.Unmarshal(r.Body, v); err != nil {
157+
return err
158+
}
159+
return nil
160+
}
161+
162+
// ErrorParser is a function that is used to construct custom error messages.
163+
type ErrorParser func([]byte) string
164+
165+
// HTTPOption is an additional parameter that can be specified to customize an outgoing request.
166+
type HTTPOption func(*http.Request)
167+
168+
// WithHeader creates an HTTPOption that will set an HTTP header on the request.
169+
func WithHeader(key, value string) HTTPOption {
170+
return func(r *http.Request) {
171+
r.Header.Set(key, value)
172+
}
173+
}
174+
175+
// WithQueryParam creates an HTTPOption that will set a query parameter on the request.
176+
func WithQueryParam(key, value string) HTTPOption {
177+
return func(r *http.Request) {
178+
q := r.URL.Query()
179+
q.Add(key, value)
180+
r.URL.RawQuery = q.Encode()
181+
}
182+
}
183+
184+
// WithQueryParams creates an HTTPOption that will set all the entries of qp as query parameters
185+
// on the request.
186+
func WithQueryParams(qp map[string]string) HTTPOption {
187+
return func(r *http.Request) {
188+
q := r.URL.Query()
189+
for k, v := range qp {
190+
q.Add(k, v)
191+
}
192+
r.URL.RawQuery = q.Encode()
193+
}
194+
}

0 commit comments

Comments
 (0)