github.com/apcera/util@v0.0.0-20180322191801-7a50bc84ee48/restclient/restclient.go (about) 1 // Copyright 2013-2014 Apcera Inc. All rights reserved. 2 3 // Package restclient wraps a REST-ful web service to expose objects from the 4 // service in Go programs. Construct a client using 5 // restclient.New("http://service.com/api/endpoint"). Use the client's HTTP-verb 6 // methods to receive the result of REST operations in a Go type. For example, 7 // to get a collection of Items, invoke client.Get("items", m) where m is of 8 // type []Item. 9 // 10 // The package also exposes lower level interfaces to receive the raw 11 // http.Response from the client and to construct requests to a client's service 12 // that may be sent later, or by an alternate client or transport. 13 package restclient 14 15 import ( 16 "bytes" 17 "encoding/json" 18 "fmt" 19 "io" 20 "io/ioutil" 21 "mime" 22 "net" 23 "net/http" 24 "net/url" 25 "path" 26 "strings" 27 "time" 28 ) 29 30 // Method wraps HTTP verbs for stronger typing. 31 type Method string 32 33 // HTTP methods for REST 34 const ( 35 GET = Method("GET") 36 POST = Method("POST") 37 PUT = Method("PUT") 38 DELETE = Method("DELETE") 39 ) 40 41 const ( 42 vndMediaTypePrefix = "application/vnd" 43 mediaTypeJSONSuffix = "+json" 44 ) 45 46 // Client represents a client bound to a given REST base URL. 47 type Client struct { 48 // Driver is the *http.Client that performs requests. 49 Driver *http.Client 50 // base is the URL under which all REST-ful resources are available. 51 base *url.URL 52 // Headers represents common headers that are added to each request. 53 Headers http.Header 54 // KeepAlives enabled 55 KeepAlives bool 56 } 57 58 // New returns a *Client with the specified base URL endpoint, expected to 59 // include the port string and any path, if required. Returns an error if 60 // baseurl cannot be parsed as an absolute URL. 61 func New(baseurl string) (*Client, error) { 62 base, err := url.ParseRequestURI(baseurl) 63 if err != nil { 64 return nil, err 65 } else if !base.IsAbs() || base.Host == "" { 66 return nil, fmt.Errorf("URL is not absolute: %s", baseurl) 67 } 68 69 // create the client 70 client := &Client{ 71 Driver: &http.Client{}, // Don't use default client; shares by reference 72 Headers: http.Header(make(map[string][]string)), 73 base: base, 74 KeepAlives: true, 75 } 76 77 return client, nil 78 } 79 80 // NewWithoutKeepAlives returns a new client with keepalives disabled. 81 func NewDisableKeepAlives(baseurl string) (*Client, error) { 82 base, err := url.ParseRequestURI(baseurl) 83 if err != nil { 84 return nil, err 85 } else if !base.IsAbs() || base.Host == "" { 86 return nil, fmt.Errorf("URL is not absolute: %s", baseurl) 87 } 88 89 transport := http.DefaultTransport.(*http.Transport) 90 transport.DisableKeepAlives = true 91 92 // create the client 93 client := &Client{ 94 Driver: &http.Client{ 95 Transport: transport, 96 }, // Don't use default client; shares by reference 97 Headers: http.Header(make(map[string][]string)), 98 base: base, 99 KeepAlives: false, 100 } 101 102 return client, nil 103 } 104 105 // BaseURL returns a *url.URL to a copy of Client's base so the caller may 106 // modify it. 107 func (c *Client) BaseURL() *url.URL { 108 return c.base.ResolveReference(&url.URL{}) 109 } 110 111 // Set the access Token 112 func (c *Client) SetAccessToken(token string) { 113 c.Headers.Set(http.CanonicalHeaderKey("Authorization"), "Bearer "+token) 114 } 115 116 // SetTimeout sets the timeout of a client to the given duration. 117 func (c *Client) SetTimeout(duration time.Duration) { 118 c.Driver.Timeout = duration 119 } 120 121 // Get issues a GET request to the specified endpoint and parses the response 122 // into resp. It will return an error if it failed to send the request, a 123 // *RestError if the response wasn't a 2xx status code, or an error from package 124 // json's Decode. 125 func (c *Client) Get(endpoint string, resp interface{}) error { 126 return c.Result(c.NewJsonRequest(GET, endpoint, nil), resp) 127 } 128 129 // Post issues a POST request to the specified endpoint with the req payload 130 // marshaled to JSON and parses the response into resp. It will return an error 131 // if it failed to send the request, a *RestError if the response wasn't a 2xx 132 // status code, or an error from package json's Decode. 133 func (c *Client) Post(endpoint string, req interface{}, resp interface{}) error { 134 return c.Result(c.NewJsonRequest(POST, endpoint, req), resp) 135 } 136 137 // Put issues a PUT request to the specified endpoint with the req payload 138 // marshaled to JSON and parses the response into resp. It will return an error 139 // if it failed to send the request, a *RestError if the response wasn't a 2xx 140 // status code, or an error from package json's Decode. 141 func (c *Client) Put(endpoint string, req interface{}, resp interface{}) error { 142 return c.Result(c.NewJsonRequest(PUT, endpoint, req), resp) 143 } 144 145 // Delete issues a DELETE request to the specified endpoint and parses the 146 // response in to resp. It will return an error if it failed to send the request, a 147 // *RestError if the response wasn't a 2xx status code, or an error from package 148 // json's Decode. 149 func (c *Client) Delete(endpoint string, resp interface{}) error { 150 return c.Result(c.NewJsonRequest(DELETE, endpoint, nil), resp) 151 } 152 153 // Result performs the request described by req and unmarshals a successful 154 // HTTP response into resp. If resp is nil, the response is discarded. 155 func (c *Client) Result(req *Request, resp interface{}) error { 156 result, err := c.Do(req) 157 if err != nil { 158 return err 159 } 160 return unmarshal(result, resp) 161 } 162 163 // Do performs the HTTP request described by req and returns the *http.Response. 164 // Also returns a non-nil *RestError if an error occurs or the response is not 165 // in the 2xx family. 166 func (c *Client) Do(req *Request) (*http.Response, error) { 167 hreq, err := req.HTTPRequest() 168 if err != nil { 169 return nil, &RestError{Req: hreq, err: fmt.Errorf("error preparing request: %s", err)} 170 } 171 172 if !c.KeepAlives { 173 hreq.Close = true 174 } 175 176 // Internally, this uses c.Driver's CheckRedirect policy. 177 resp, err := c.Driver.Do(hreq) 178 if err != nil { 179 if opErr, ok := err.(*net.OpError); ok { 180 if opErr.Timeout() { 181 return nil, &RestError{Req: hreq, err: fmt.Errorf("timed out making request")} 182 } 183 } 184 return resp, &RestError{Req: hreq, Resp: resp, err: fmt.Errorf("error sending request: %s", err)} 185 } 186 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 187 return resp, &RestError{Req: hreq, Resp: resp, err: fmt.Errorf("error in response: %s", resp.Status)} 188 } 189 return resp, nil 190 } 191 192 // NewRequest generates a new Request object that will send bytes read from body 193 // to the endpoint. 194 func (c *Client) NewRequest(method Method, endpoint string, ctype string, body io.Reader) (req *Request) { 195 req = c.newRequest(method, endpoint) 196 if body == nil { 197 return 198 } 199 200 req.prepare = func(hr *http.Request) error { 201 rc, ok := body.(io.ReadCloser) 202 if !ok { 203 rc = ioutil.NopCloser(body) 204 } 205 hr.Body = rc 206 hr.Header.Set("Content-Type", ctype) 207 return nil 208 } 209 return 210 } 211 212 // NewJsonRequest generates a new Request object and JSON encodes the provided 213 // obj. The JSON object will be set as the body and included in the request. 214 func (c *Client) NewJsonRequest(method Method, endpoint string, obj interface{}) (req *Request) { 215 req = c.newRequest(method, endpoint) 216 if obj == nil { 217 return 218 } 219 220 req.prepare = func(httpReq *http.Request) error { 221 var buffer bytes.Buffer 222 encoder := json.NewEncoder(&buffer) 223 if err := encoder.Encode(obj); err != nil { 224 return err 225 } 226 227 // set to the request 228 httpReq.Body = ioutil.NopCloser(&buffer) 229 httpReq.ContentLength = int64(buffer.Len()) 230 httpReq.Header.Set("Content-Type", "application/json") 231 return nil 232 } 233 234 return req 235 } 236 237 // NewFormRequest generates a new Request object with a form encoded body based 238 // on the params map. 239 func (c *Client) NewFormRequest(method Method, endpoint string, params map[string]string) *Request { 240 req := c.newRequest(method, endpoint) 241 242 // set how to generate the body 243 req.prepare = func(httpReq *http.Request) error { 244 form := url.Values{} 245 for k, v := range params { 246 form.Set(k, v) 247 } 248 encoded := form.Encode() 249 250 // set to the request 251 httpReq.Body = ioutil.NopCloser(bytes.NewReader([]byte(encoded))) 252 httpReq.ContentLength = int64(len(encoded)) 253 httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") 254 return nil 255 } 256 257 return req 258 } 259 260 // newRequest returns a *Request ready to be used by one of Client's exported 261 // methods like NewFormRequest. 262 func (c *Client) newRequest(method Method, endpoint string) *Request { 263 req := &Request{ 264 Method: method, 265 URL: resourceURL(c.BaseURL(), endpoint), 266 Headers: http.Header(make(map[string][]string)), 267 } 268 269 // Copy over the headers. Don't set them directly to ensure changing 270 // them on the request doesn't change them on the client. 271 for k, vv := range c.Headers { 272 for _, v := range vv { 273 req.Headers.Add(k, v) 274 } 275 } 276 277 return req 278 } 279 280 // Request encapsulates functionality making it easier to build REST requests. 281 type Request struct { 282 Method Method 283 URL *url.URL 284 Headers http.Header 285 286 prepare func(*http.Request) error 287 } 288 289 // HTTPRequest returns an *http.Request populated with data from r. It may be 290 // executed by any http.Client. 291 func (r *Request) HTTPRequest() (*http.Request, error) { 292 req, err := http.NewRequest(string(r.Method), r.URL.String(), nil) 293 if err != nil { 294 return nil, err 295 } 296 297 // merge headers 298 req.Header = r.Headers 299 300 // generate the body 301 if r.prepare != nil { 302 if err := r.prepare(req); err != nil { 303 return nil, err 304 } 305 } 306 307 return req, nil 308 } 309 310 // resourceURL returns a *url.URL with the path resolved for a resource under base. 311 func resourceURL(base *url.URL, relPath string) *url.URL { 312 relPath, rawQuery := splitPathQuery(relPath) 313 ref := &url.URL{Path: path.Join(base.Path, relPath), RawQuery: rawQuery} 314 return base.ResolveReference(ref) 315 } 316 317 func splitPathQuery(relPath string) (retPath, rawQuery string) { 318 parsedPath, _ := url.Parse(relPath) 319 rawQuery = parsedPath.RawQuery 320 retPath = strings.TrimSuffix(relPath, fmt.Sprintf("?%s", rawQuery)) 321 return 322 } 323 324 // unmarshal unmarshals a JSON object from the response object's body. If the 325 // Content-Type is not application/json or application/vnd* (or we can't detect 326 // the media type) an error is returned. The response body is always closed. 327 func unmarshal(resp *http.Response, v interface{}) error { 328 defer resp.Body.Close() 329 330 // Don't Unmarshal Body if v is nil 331 if v == nil { 332 return nil 333 } 334 335 ctype, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 336 if err != nil { 337 return err 338 } 339 340 if !isJSONContentType(ctype) { 341 return fmt.Errorf("unexpected response: %s %s", resp.Status, ctype) 342 } 343 344 return json.NewDecoder(resp.Body).Decode(v) 345 } 346 347 // isJSONContentType returns whether or not the media type should be expected to 348 // contain JSON. Explicit application/json as well as types of the form 349 // application/vnd* and *+json are permitted. The simple checks prevent us from 350 // running http.DetectContentType on every request or trying to decode things 351 // that are clearly not JSON. 352 func isJSONContentType(mediaType string) bool { 353 if mediaType == "application/json" || 354 strings.HasPrefix(mediaType, vndMediaTypePrefix) || 355 strings.HasSuffix(mediaType, mediaTypeJSONSuffix) { 356 return true 357 } 358 return false 359 } 360 361 // RestError is returned from REST transmissions to allow for inspection of 362 // failed request and response contents. 363 type RestError struct { 364 // The Request that triggered the error. 365 Req *http.Request 366 // The Resposne that the request returned. 367 Resp *http.Response 368 // err is the original error 369 err error 370 // ErrBody is the body of the request that errored. 371 // Not named Body since there is an accessor method. 372 ErrBody *string 373 } 374 375 func (r *RestError) Error() string { 376 msg := r.err.Error() 377 prefix := msg + " - " 378 379 // Make sure the Error reads the cached body so 380 // you can call error multiple times with no issues. 381 // Also handle json from the endpoint and look for 382 // the error field. 383 if r.Body() != "" { 384 type body struct { 385 Error string `json:"error"` 386 } 387 388 var b *body 389 390 jerr := json.Unmarshal([]byte(r.Body()), &b) 391 if jerr != nil { 392 return prefix + r.Body() 393 } 394 395 if b.Error != "" { 396 return prefix + b.Error 397 } 398 399 return prefix + r.Body() 400 } 401 402 return msg 403 } 404 405 func (r *RestError) Body() string { 406 // Return the body if we have it. 407 if r.ErrBody != nil { 408 return *r.ErrBody 409 } 410 411 // Easier to deal with body as regular string. 412 // ErrBody is a pointer so that I can tell if it was 413 // actually set to "". 414 strBody := "" 415 416 // If we don't have a body, return "". 417 if r.Resp == nil || r.Resp.Body == nil { 418 r.ErrBody = &strBody 419 return *r.ErrBody 420 } 421 422 // Read the body, then set a new buffer 423 // to the body field so the original 424 // response still has a body. 425 b, _ := ioutil.ReadAll(r.Resp.Body) 426 defer r.Resp.Body.Close() 427 buf := bytes.NewBuffer(b) 428 r.Resp.Body = ioutil.NopCloser(buf) 429 430 // Set ErrBody to the new body. 431 strBody = string(b) 432 r.ErrBody = &strBody 433 434 return *r.ErrBody 435 }