~rogpeppe/goose/state-of-the-world

« back to all changes in this revision

Viewing changes to http/client.go

  • Committer: Ian Booth
  • Date: 2012-11-21 07:56:19 UTC
  • Revision ID: ian.booth@canonical.com-20121121075619-4fh6i9yq6fj6cwct
Extract identity functionality from client, and also extract common HTTP methods

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// An HTTP Client which sends json and binary requests, handling data marshalling and response processing.
 
2
 
 
3
package http
 
4
 
 
5
import (
 
6
        "encoding/json"
 
7
        "errors"
 
8
        "fmt"
 
9
        gooseerrors "launchpad.net/goose/errors"
 
10
        "io/ioutil"
 
11
        "net/http"
 
12
        "strings"
 
13
        "bytes"
 
14
)
 
15
 
 
16
type GooseHTTPClient struct {
 
17
        http.Client
 
18
}
 
19
 
 
20
type ErrorResponse struct {
 
21
        Message string `json:"message"`
 
22
        Code    int    `json:"code"`
 
23
        Title   string `json:"title"`
 
24
}
 
25
 
 
26
func (e *ErrorResponse) Error() string {
 
27
        return fmt.Sprintf("Failed: %d %s: %s", e.Code, e.Title, e.Message)
 
28
}
 
29
 
 
30
type ErrorWrapper struct {
 
31
        Error ErrorResponse `json:"error"`
 
32
}
 
33
 
 
34
type RequestData struct {
 
35
        ReqHeaders http.Header
 
36
        ExpectedStatus []int
 
37
        ReqValue interface{}
 
38
        RespValue interface{}
 
39
        ReqData []byte
 
40
        RespData *[]byte
 
41
}
 
42
 
 
43
// JsonRequest JSON encodes and sends the supplied object (if any) to the specified URL.
 
44
// Optional method arguments are pass using the RequestData object.
 
45
// Relevant RequestData fields:
 
46
// ReqHeaders: additional HTTP header values to add to the request.
 
47
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
 
48
// ReqValue: the data object to send.
 
49
// RespValue: the data object to decode the result into.
 
50
func (c *GooseHTTPClient) JsonRequest(method, url string, reqData *RequestData) (err error) {
 
51
        err = nil
 
52
        var (
 
53
                req      *http.Request
 
54
                body []byte
 
55
        )
 
56
        if reqData.ReqValue != nil {
 
57
                body, err = json.Marshal(reqData.ReqValue)
 
58
                if err != nil {
 
59
                        gooseerrors.AddErrorContext(&err, "failed marshalling the request body")
 
60
                        return
 
61
                }
 
62
                reqBody := strings.NewReader(string(body))
 
63
                req, err = http.NewRequest(method, url, reqBody)
 
64
        } else {
 
65
                req, err = http.NewRequest(method, url, nil)
 
66
        }
 
67
        if err != nil {
 
68
                gooseerrors.AddErrorContext(&err, "failed creating the request")
 
69
                return
 
70
        }
 
71
        req.Header.Add("Content-Type", "application/json")
 
72
        req.Header.Add("Accept", "application/json")
 
73
 
 
74
        respBody, err := c.sendRequest(req, reqData.ReqHeaders, reqData.ExpectedStatus, string(body))
 
75
        if err != nil {
 
76
                return
 
77
        }
 
78
 
 
79
        if len(respBody) > 0 {
 
80
                if reqData.RespValue != nil {
 
81
                        err = json.Unmarshal(respBody, &reqData.RespValue)
 
82
                        if err != nil {
 
83
                                gooseerrors.AddErrorContext(&err, "failed unmarshaling the response body: %s", respBody)
 
84
                        }
 
85
                }
 
86
        }
 
87
        return
 
88
}
 
89
 
 
90
// Sends the supplied byte array (if any) to the specified URL.
 
91
// Optional method arguments are pass using the RequestData object.
 
92
// Relevant RequestData fields:
 
93
// ReqHeaders: additional HTTP header values to add to the request.
 
94
// ExpectedStatus: the allowed HTTP response status values, else an error is returned.
 
95
// ReqData: the byte array to send.
 
96
// RespData: the byte array to decode the result into.
 
97
func (c *GooseHTTPClient) BinaryRequest(method, url string, reqData *RequestData) (err error) {
 
98
        err = nil
 
99
 
 
100
        var req *http.Request
 
101
 
 
102
        if reqData.ReqData != nil {
 
103
                rawReqReader := bytes.NewReader(reqData.ReqData)
 
104
                req, err = http.NewRequest(method, url, rawReqReader)
 
105
        } else {
 
106
                req, err = http.NewRequest(method, url, nil)
 
107
        }
 
108
        if err != nil {
 
109
                gooseerrors.AddErrorContext(&err, "failed creating the request")
 
110
                return
 
111
        }
 
112
        req.Header.Add("Content-Type", "application/octet-stream")
 
113
        req.Header.Add("Accept", "application/octet-stream")
 
114
 
 
115
        respBody, err := c.sendRequest(req, reqData.ReqHeaders, reqData.ExpectedStatus, string(reqData.ReqData))
 
116
        if err != nil {
 
117
                return
 
118
        }
 
119
 
 
120
        if len(respBody) > 0 {
 
121
                if reqData.RespData != nil {
 
122
                        *reqData.RespData = respBody
 
123
                }
 
124
        }
 
125
        return
 
126
}
 
127
 
 
128
// Sends the specified request and checks that the HTTP response status is as expected.
 
129
// req: the request to send.
 
130
// extraHeaders: additional HTTP headers to include with the request.
 
131
// expectedStatus: a slice of allowed response status codes.
 
132
// payloadInfo: a string to include with an error message if something goes wrong.
 
133
func (c *GooseHTTPClient) sendRequest(req *http.Request, extraHeaders http.Header, expectedStatus []int, payloadInfo string) (respBody []byte, err error) {
 
134
        if extraHeaders != nil {
 
135
                for header, values := range extraHeaders {
 
136
                        for _, value := range values {
 
137
                                req.Header.Add(header, value)
 
138
                        }
 
139
                }
 
140
        }
 
141
 
 
142
        rawResp, err := c.Do(req)
 
143
        if err != nil {
 
144
                gooseerrors.AddErrorContext(&err, "failed executing the request")
 
145
                return
 
146
        }
 
147
        foundStatus := false
 
148
        if len(expectedStatus) == 0 {
 
149
                expectedStatus = []int{http.StatusOK}
 
150
        }
 
151
        for _, status := range expectedStatus {
 
152
                if rawResp.StatusCode == status {
 
153
                        foundStatus = true
 
154
                        break
 
155
                }
 
156
        }
 
157
        if !foundStatus && len(expectedStatus) > 0 {
 
158
                defer rawResp.Body.Close()
 
159
                var errInfo interface{}
 
160
                errInfo, _ = ioutil.ReadAll(rawResp.Body)
 
161
                // Check if we have a JSON representation of the failure, if so decode it.
 
162
                if rawResp.Header.Get("Content-Type") == "application/json" {
 
163
                        var wrappedErr ErrorWrapper
 
164
                        if err := json.Unmarshal(errInfo.([]byte), &wrappedErr); err == nil {
 
165
                                errInfo = wrappedErr.Error
 
166
                        }
 
167
                }
 
168
                err = errors.New(
 
169
                fmt.Sprintf(
 
170
                        "request (%s) returned unexpected status: %s; error info: %v; request body: %s",
 
171
                        req.URL,
 
172
                        rawResp.Status,
 
173
                        errInfo,
 
174
                        payloadInfo))
 
175
                return
 
176
        }
 
177
 
 
178
        respBody, err = ioutil.ReadAll(rawResp.Body)
 
179
        rawResp.Body.Close()
 
180
        if err != nil {
 
181
                gooseerrors.AddErrorContext(&err, "failed reading the response body")
 
182
                return
 
183
        }
 
184
        return
 
185
}