~gophers/goose/unstable-001

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
// An HTTP Client which sends json and binary requests, handling data marshalling and response processing.

package http

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	gooseerrors "launchpad.net/~gophers/goose/unstable-001/errors"
	"net/http"
	"net/url"
	"strings"
)

type Client struct {
	http.Client
	AuthToken string
}

type ErrorResponse struct {
	Message string `json:"message"`
	Code    int    `json:"code"`
	Title   string `json:"title"`
}

func (e *ErrorResponse) Error() string {
	return fmt.Sprintf("Failed: %d %s: %s", e.Code, e.Title, e.Message)
}

type ErrorWrapper struct {
	Error ErrorResponse `json:"error"`
}

type RequestData struct {
	ReqHeaders     http.Header
	Params         *url.Values
	ExpectedStatus []int
	ReqValue       interface{}
	RespValue      interface{}
	ReqData        []byte
	RespData       *[]byte
}

// JsonRequest JSON encodes and sends the supplied object (if any) to the specified URL.
// Optional method arguments are pass using the RequestData object.
// Relevant RequestData fields:
// ReqHeaders: additional HTTP header values to add to the request.
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
// ReqValue: the data object to send.
// RespValue: the data object to decode the result into.
func (c *Client) JsonRequest(method, url string, reqData *RequestData) (err error) {
	err = nil
	var (
		req  *http.Request
		body []byte
	)
	if reqData.Params != nil {
		url += "?" + reqData.Params.Encode()
	}
	if reqData.ReqValue != nil {
		body, err = json.Marshal(reqData.ReqValue)
		if err != nil {
			err = gooseerrors.AddContext(err, "failed marshalling the request body")
			return
		}
		reqBody := strings.NewReader(string(body))
		req, err = http.NewRequest(method, url, reqBody)
	} else {
		req, err = http.NewRequest(method, url, nil)
	}
	if err != nil {
		err = gooseerrors.AddContext(err, "failed creating the request")
		return
	}
	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Accept", "application/json")

	respBody, err := c.sendRequest(req, reqData.ReqHeaders, reqData.ExpectedStatus, string(body))
	if err != nil {
		return
	}

	if len(respBody) > 0 {
		if reqData.RespValue != nil {
			err = json.Unmarshal(respBody, &reqData.RespValue)
			if err != nil {
				err = gooseerrors.AddContext(err, "failed unmarshaling the response body: %s", respBody)
			}
		}
	}
	return
}

// Sends the supplied byte array (if any) to the specified URL.
// Optional method arguments are pass using the RequestData object.
// Relevant RequestData fields:
// ReqHeaders: additional HTTP header values to add to the request.
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
// ReqData: the byte array to send.
// RespData: the byte array to decode the result into.
func (c *Client) BinaryRequest(method, url string, reqData *RequestData) (err error) {
	err = nil

	var req *http.Request

	if reqData.Params != nil {
		url += "?" + reqData.Params.Encode()
	}
	if reqData.ReqData != nil {
		rawReqReader := bytes.NewReader(reqData.ReqData)
		req, err = http.NewRequest(method, url, rawReqReader)
	} else {
		req, err = http.NewRequest(method, url, nil)
	}
	if err != nil {
		err = gooseerrors.AddContext(err, "failed creating the request")
		return
	}
	req.Header.Add("Content-Type", "application/octet-stream")
	req.Header.Add("Accept", "application/octet-stream")

	respBody, err := c.sendRequest(req, reqData.ReqHeaders, reqData.ExpectedStatus, string(reqData.ReqData))
	if err != nil {
		return
	}

	if len(respBody) > 0 {
		if reqData.RespData != nil {
			*reqData.RespData = respBody
		}
	}
	return
}

// Sends the specified request and checks that the HTTP response status is as expected.
// req: the request to send.
// extraHeaders: additional HTTP headers to include with the request.
// expectedStatus: a slice of allowed response status codes.
// payloadInfo: a string to include with an error message if something goes wrong.
func (c *Client) sendRequest(req *http.Request, extraHeaders http.Header, expectedStatus []int, payloadInfo string) (respBody []byte, err error) {
	if extraHeaders != nil {
		for header, values := range extraHeaders {
			for _, value := range values {
				req.Header.Add(header, value)
			}
		}
	}
	if c.AuthToken != "" {
		req.Header.Add("X-Auth-Token", c.AuthToken)
	}
	rawResp, err := c.Do(req)
	if err != nil {
		err = gooseerrors.AddContext(err, "failed executing the request")
		return
	}
	foundStatus := false
	if len(expectedStatus) == 0 {
		expectedStatus = []int{http.StatusOK}
	}
	for _, status := range expectedStatus {
		if rawResp.StatusCode == status {
			foundStatus = true
			break
		}
	}
	defer rawResp.Body.Close()
	if !foundStatus && len(expectedStatus) > 0 {
		err = handleError(req.URL, rawResp, payloadInfo)
		return
	}

	respBody, err = ioutil.ReadAll(rawResp.Body)
	if err != nil {
		err = gooseerrors.AddContext(err, "failed reading the response body")
		return
	}
	return
}

// The HTTP response status code was not one of those expected, so we construct an error.
// NotFound (404) codes have their own NotFound error type.
func handleError(URL *url.URL, resp *http.Response, payloadInfo string) error {
	var errInfo interface{}
	errBytes, _ := ioutil.ReadAll(resp.Body)
	errInfo = string(errBytes)
	// Check if we have a JSON representation of the failure, if so decode it.
	if resp.Header.Get("Content-Type") == "application/json" {
		var wrappedErr ErrorWrapper
		if err := json.Unmarshal(errBytes, &wrappedErr); err == nil {
			errInfo = wrappedErr.Error
		}
	}
	if resp.StatusCode == http.StatusNotFound {
		return gooseerrors.NotFound("resource at URL %s", URL)
	}
	err := errors.New(
		fmt.Sprintf(
			"request (%s) returned unexpected status: %s; error info: %v; request body: [%s]",
			URL,
			resp.Status,
			errInfo,
			payloadInfo))
	return err
}