1
// An HTTP Client which sends json and binary requests, handling data marshalling and response processing.
21
"gopkg.in/goose.v1/errors"
25
contentTypeJSON = "application/json"
26
contentTypeOctetStream = "application/octet-stream"
30
// See https://code.google.com/p/go/issues/detail?id=4677
31
// We need to force the connection to close each time so that we don't
32
// hit the above Go bug.
33
roundTripper := http.DefaultClient.Transport
34
if transport, ok := roundTripper.(*http.Transport); ok {
35
transport.DisableKeepAlives = true
37
http.DefaultTransport.(*http.Transport).DisableKeepAlives = true
45
type ErrorResponse struct {
46
Message string `json:"message"`
47
Code int `json:"code"`
48
Title string `json:"title"`
51
func (e *ErrorResponse) Error() string {
52
return fmt.Sprintf("Failed: %d %s: %s", e.Code, e.Title, e.Message)
55
func unmarshallError(jsonBytes []byte) (*ErrorResponse, error) {
56
var response ErrorResponse
57
var transientObject = make(map[string]*json.RawMessage)
58
if err := json.Unmarshal(jsonBytes, &transientObject); err != nil {
61
for key, value := range transientObject {
62
if err := json.Unmarshal(*value, &response); err != nil {
68
if response.Code != 0 && response.Message != "" {
71
return nil, fmt.Errorf("Unparsable json error body: %q", jsonBytes)
74
type RequestData struct {
75
ReqHeaders http.Header
82
RespReader io.ReadCloser
83
RespHeaders http.Header
87
// The maximum number of times to try sending a request before we give up
88
// (assuming any unsuccessful attempts can be sensibly tried again).
92
var insecureClient *http.Client
93
var insecureClientMutex sync.Mutex
95
// New returns a new goose http *Client using the default net/http client.
97
return &Client{*http.DefaultClient, MaxSendAttempts}
100
func NewNonSSLValidating() *Client {
101
insecureClientMutex.Lock()
102
httpClient := insecureClient
103
if httpClient == nil {
104
insecureConfig := &tls.Config{InsecureSkipVerify: true}
105
insecureTransport := &http.Transport{TLSClientConfig: insecureConfig}
106
insecureClient = &http.Client{Transport: insecureTransport}
107
httpClient = insecureClient
109
insecureClientMutex.Unlock()
110
return &Client{*httpClient, MaxSendAttempts}
113
func gooseAgent() string {
114
return fmt.Sprintf("goose (%s)", goose.Version)
117
func createHeaders(extraHeaders http.Header, contentType, authToken string) http.Header {
118
headers := make(http.Header)
119
if extraHeaders != nil {
120
for header, values := range extraHeaders {
121
for _, value := range values {
122
headers.Add(header, value)
127
headers.Set("X-Auth-Token", authToken)
129
headers.Add("Content-Type", contentType)
130
headers.Add("Accept", contentType)
131
headers.Add("User-Agent", gooseAgent())
135
// JsonRequest JSON encodes and sends the object in reqData.ReqValue (if any) to the specified URL.
136
// Optional method arguments are passed using the RequestData object.
137
// Relevant RequestData fields:
138
// ReqHeaders: additional HTTP header values to add to the request.
139
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
140
// ReqValue: the data object to send.
141
// RespValue: the data object to decode the result into.
142
func (c *Client) JsonRequest(method, url, token string, reqData *RequestData, logger *log.Logger) (err error) {
145
if reqData.Params != nil {
146
url += "?" + reqData.Params.Encode()
148
if reqData.ReqValue != nil {
149
body, err = json.Marshal(reqData.ReqValue)
151
err = errors.Newf(err, "failed marshalling the request body")
155
headers := createHeaders(reqData.ReqHeaders, contentTypeJSON, token)
156
resp, err := c.sendRequest(
157
method, url, bytes.NewReader(body), len(body), headers, reqData.ExpectedStatus, logger)
161
reqData.RespHeaders = resp.Header
162
defer resp.Body.Close()
163
respData, err := ioutil.ReadAll(resp.Body)
165
err = errors.Newf(err, "failed reading the response body")
169
if len(respData) > 0 {
170
if reqData.RespValue != nil {
171
err = json.Unmarshal(respData, &reqData.RespValue)
173
err = errors.Newf(err, "failed unmarshaling the response body: %s", respData)
180
// Sends the byte array in reqData.ReqValue (if any) to the specified URL.
181
// Optional method arguments are passed using the RequestData object.
182
// Relevant RequestData fields:
183
// ReqHeaders: additional HTTP header values to add to the request.
184
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
185
// ReqReader: an io.Reader providing the bytes to send.
186
// RespReader: assigned an io.ReadCloser instance used to read the returned data..
187
func (c *Client) BinaryRequest(method, url, token string, reqData *RequestData, logger *log.Logger) (err error) {
190
if reqData.Params != nil {
191
url += "?" + reqData.Params.Encode()
193
headers := createHeaders(reqData.ReqHeaders, contentTypeOctetStream, token)
194
resp, err := c.sendRequest(
195
method, url, reqData.ReqReader, reqData.ReqLength, headers, reqData.ExpectedStatus, logger)
199
reqData.RespHeaders = resp.Header
200
if reqData.RespReader != nil {
201
reqData.RespReader = resp.Body
208
// Sends the specified request to URL and checks that the HTTP response status is as expected.
209
// reqReader: a reader returning the data to send.
210
// length: the number of bytes to send.
211
// headers: HTTP headers to include with the request.
212
// expectedStatus: a slice of allowed response status codes.
213
func (c *Client) sendRequest(method, URL string, reqReader io.Reader, length int, headers http.Header,
214
expectedStatus []int, logger *log.Logger) (*http.Response, error) {
215
reqData := make([]byte, length)
216
if reqReader != nil {
217
nrRead, err := io.ReadFull(reqReader, reqData)
219
err = errors.Newf(err, "failed reading the request data, read %v of %v bytes", nrRead, length)
223
rawResp, err := c.sendRateLimitedRequest(method, URL, headers, reqData, logger)
228
if len(expectedStatus) == 0 {
229
expectedStatus = []int{http.StatusOK}
231
for _, status := range expectedStatus {
232
if rawResp.StatusCode == status {
237
if !foundStatus && len(expectedStatus) > 0 {
238
err = handleError(URL, rawResp)
245
func (c *Client) sendRateLimitedRequest(method, URL string, headers http.Header, reqData []byte,
246
logger *log.Logger) (resp *http.Response, err error) {
247
for i := 0; i < c.maxSendAttempts; i++ {
248
var reqReader io.Reader
250
reqReader = bytes.NewReader(reqData)
252
req, err := http.NewRequest(method, URL, reqReader)
254
err = errors.Newf(err, "failed creating the request %s", URL)
257
for header, values := range headers {
258
for _, value := range values {
259
req.Header.Add(header, value)
262
req.ContentLength = int64(len(reqData))
263
resp, err = c.Do(req)
265
return nil, errors.Newf(err, "failed executing the request %s", URL)
267
if resp.StatusCode != http.StatusRequestEntityTooLarge || resp.Header.Get("Retry-After") == "" {
271
retryAfter, err := strconv.ParseFloat(resp.Header.Get("Retry-After"), 32)
273
return nil, errors.Newf(err, "Invalid Retry-After header %s", URL)
276
return nil, errors.Newf(err, "Resource limit exeeded at URL %s", URL)
279
logger.Printf("Too many requests, retrying in %dms.", int(retryAfter*1000))
281
time.Sleep(time.Duration(retryAfter) * time.Second)
283
return nil, errors.Newf(err, "Maximum number of attempts (%d) reached sending request to %s", c.maxSendAttempts, URL)
286
type HttpError struct {
288
Data map[string][]string
290
responseMessage string
293
func (e *HttpError) Error() string {
294
return fmt.Sprintf("request (%s) returned unexpected status: %d; error info: %v",
301
// The HTTP response status code was not one of those expected, so we construct an error.
302
// NotFound (404) codes have their own NotFound error type.
303
// We also make a guess at duplicate value errors.
304
func handleError(URL string, resp *http.Response) error {
305
errBytes, _ := ioutil.ReadAll(resp.Body)
306
errInfo := string(errBytes)
307
// Check if we have a JSON representation of the failure, if so decode it.
308
if resp.Header.Get("Content-Type") == contentTypeJSON {
309
errorResponse, err := unmarshallError(errBytes)
310
//TODO (hduran-8): Obtain a logger and log the error
312
errInfo = errorResponse.Error()
315
httpError := &HttpError{
316
resp.StatusCode, map[string][]string(resp.Header), URL, errInfo,
318
switch resp.StatusCode {
319
case http.StatusNotFound:
320
return errors.NewNotFoundf(httpError, "", "Resource at %s not found", URL)
321
case http.StatusForbidden, http.StatusUnauthorized:
322
return errors.NewUnauthorisedf(httpError, "", "Unauthorised URL %s", URL)
323
case http.StatusBadRequest:
324
dupExp, _ := regexp.Compile(".*already exists.*")
325
if dupExp.Match(errBytes) {
326
return errors.NewDuplicateValuef(httpError, "", string(errBytes))