Chromium Code Reviews| Index: client/internal/lhttp/client.go | 
| diff --git a/client/internal/lhttp/client.go b/client/internal/lhttp/client.go | 
| new file mode 100644 | 
| index 0000000000000000000000000000000000000000..c80690f768f8c2610b076ae5061de28807caf150 | 
| --- /dev/null | 
| +++ b/client/internal/lhttp/client.go | 
| @@ -0,0 +1,172 @@ | 
| +// Copyright 2015 The Chromium Authors. All rights reserved. | 
| +// Use of this source code is governed by a BSD-style license that can be | 
| +// found in the LICENSE file. | 
| + | 
| +package lhttp | 
| + | 
| +import ( | 
| + "bytes" | 
| + "encoding/json" | 
| + "errors" | 
| + "fmt" | 
| + "io" | 
| + "io/ioutil" | 
| + "net/http" | 
| + "net/url" | 
| + "os" | 
| + "strings" | 
| + | 
| + "github.com/luci/luci-go/client/internal/retry" | 
| +) | 
| + | 
| +// Handler is called once or multiple times for each HTTP request that is tried. | 
| +type Handler func(*http.Response) error | 
| + | 
| +// NewRequest returns a retriable request. | 
| +// | 
| +// To enable automatic retry support, the Request.Body, if present, must | 
| +// implement io.Seeker. | 
| +// | 
| +// handler should return retry.Error in case of retriable error, for example if | 
| +// a TCP connection is teared off while receiving the content. | 
| +func NewRequest(c *http.Client, req *http.Request, handler Handler) (retry.Retriable, error) { | 
| + // Handle req.Body if specified. It has to implement io.Seeker. | 
| + newReq := *req | 
| + out := &httpRetriable{ | 
| + handler: handler, | 
| + c: c, | 
| + req: &newReq, | 
| + closeBody: req.Body, | 
| + } | 
| + if req.Body != nil { | 
| + ok := false | 
| + if out.seekBody, ok = req.Body.(io.Seeker); !ok { | 
| + return nil, errors.New("req.Body must implement io.Seeker") | 
| + } | 
| + // Make sure the body is not closed when calling http.Client.Do(). | 
| + out.req.Body = ioutil.NopCloser(req.Body) | 
| + } | 
| + return out, nil | 
| +} | 
| + | 
| +// NewRequestJSON returns a retriable request calling a JSON endpoint. | 
| +func NewRequestJSON(c *http.Client, url, method string, in, out interface{}) (retry.Retriable, error) { | 
| + var body io.Reader | 
| + if in != nil { | 
| + encoded, err := json.Marshal(in) | 
| + if err != nil { | 
| + return nil, err | 
| + } | 
| + body = NewReader(encoded) | 
| + } | 
| + req, err := http.NewRequest(method, url, body) | 
| + if err != nil { | 
| + return nil, err | 
| + } | 
| + if in != nil { | 
| + req.Header.Set("Content-Type", jsonContentType) | 
| + } | 
| + return NewRequest(c, req, func(resp *http.Response) error { | 
| + defer resp.Body.Close() | 
| + if ct := strings.ToLower(resp.Header.Get("Content-Type")); ct != jsonContentType { | 
| + // Non-retriable. | 
| + return fmt.Errorf("unexpected Content-Type, expected \"%s\", got \"%s\"", jsonContentType, ct) | 
| + } | 
| + if out == nil { | 
| + // The client doesn't care about the response. Still ensure the response | 
| + // is valid json. | 
| + out = &map[string]interface{}{} | 
| + } | 
| + if err := json.NewDecoder(resp.Body).Decode(out); err != nil { | 
| + // Retriable. | 
| 
 
Vadim Sh.
2015/05/13 21:54:47
ideally JSON formatting errors and network related
 
M-A Ruel
2015/05/14 01:00:54
Actually, you just create typed errors. That's exa
 
 | 
| + return retry.Error{fmt.Errorf("bad response %s: %s", url, err)} | 
| + } | 
| + return nil | 
| + }) | 
| +} | 
| + | 
| +// GetJSON is a shorthand. | 
| +func GetJSON(config *retry.Config, c *http.Client, url string, out interface{}) error { | 
| + req, err := NewRequestJSON(c, url, "GET", nil, out) | 
| + if err != nil { | 
| + return err | 
| + } | 
| + return config.Do(req) | 
| +} | 
| + | 
| +// PostJSON is a shorthand. | 
| +func PostJSON(config *retry.Config, c *http.Client, url string, in, out interface{}) error { | 
| + req, err := NewRequestJSON(c, url, "POST", in, out) | 
| + if err != nil { | 
| + return err | 
| + } | 
| + return config.Do(req) | 
| +} | 
| + | 
| +// NewReader returns a io.ReadCloser compatible read-only buffer that also | 
| 
 
Vadim Sh.
2015/05/13 21:54:47
I think it's better to keep it private. lhttp/clie
 
M-A Ruel
2015/05/14 01:00:54
Ok but it's possible we may need to expose it for
 
 | 
| +// exposes io.Seeker. This should be used instead of bytes.NewReader(), which | 
| +// doesn't implement Close(). | 
| +func NewReader(p []byte) io.ReadCloser { | 
| + return &reader{bytes.NewReader(p)} | 
| +} | 
| + | 
| +// Private details. | 
| + | 
| +const jsonContentType = "application/json; charset=utf-8" | 
| + | 
| +type reader struct { | 
| + *bytes.Reader | 
| +} | 
| + | 
| +func (r *reader) Close() error { | 
| + return nil | 
| +} | 
| + | 
| +type httpRetriable struct { | 
| + handler Handler | 
| + c *http.Client | 
| + req *http.Request | 
| + closeBody io.Closer | 
| + seekBody io.Seeker | 
| +} | 
| + | 
| +func (h *httpRetriable) Close() error { | 
| + if h.closeBody != nil { | 
| + return h.closeBody.Close() | 
| + } | 
| + return nil | 
| +} | 
| + | 
| +// Warning: it returns an error on HTTP >=400. This is different than | 
| +// http.Client.Do() but hell it makes coding simpler. | 
| +func (h *httpRetriable) Do() error { | 
| + if h.seekBody != nil { | 
| + if _, err := h.seekBody.Seek(0, os.SEEK_SET); err != nil { | 
| + // Can't be retried. | 
| + return err | 
| + } | 
| + } | 
| + resp, err := h.c.Do(h.req) | 
| + if err != nil { | 
| + // Any TCP level failure can be retried but malformed URL should nt. | 
| + if err2, ok := err.(*url.Error); ok { | 
| + return err2 | 
| + } | 
| + // http.badStringError{} is not exported. Hack around. | 
| + if strings.HasPrefix(err.Error(), "unsupported protocol scheme") { | 
| 
 
Vadim Sh.
2015/05/13 21:54:47
that's what I meant when I was saying "stupid go"
 
M-A Ruel
2015/05/14 01:00:54
Removed since I added a check earlier. It's only a
 
 | 
| + return err | 
| + } | 
| + return retry.Error{err} | 
| + } | 
| + // If the HTTP status code means the request should be retried. | 
| + if resp.StatusCode == 408 || resp.StatusCode == 429 || resp.StatusCode >= 500 { | 
| + return retry.Error{fmt.Errorf("http request failed: %s (HTTP %d)", http.StatusText(resp.StatusCode), resp.StatusCode)} | 
| + } | 
| + // Any other failure code is a hard failure. | 
| + if resp.StatusCode >= 400 { | 
| 
 
Vadim Sh.
2015/05/13 21:54:47
I may make sense to return ErrNotFound on 404 and
 
M-A Ruel
2015/05/14 01:00:54
Changed to add an interface instead if needed. The
 
 | 
| + return fmt.Errorf("http request failed: %s (HTTP %d)", http.StatusText(resp.StatusCode), resp.StatusCode) | 
| + } | 
| + // The handler may still return a retry.Error to indicate that the request | 
| + // should be retried even on successful status code. | 
| + return h.handler(resp) | 
| +} |