| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package lhttp | 5 package lhttp |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "bytes" | 8 "bytes" |
| 9 "encoding/json" | 9 "encoding/json" |
| 10 "errors" | |
| 11 "fmt" | 10 "fmt" |
| 12 "io" | 11 "io" |
| 13 "io/ioutil" | |
| 14 "log" | 12 "log" |
| 15 "net/http" | 13 "net/http" |
| 16 "os" | |
| 17 "strings" | 14 "strings" |
| 18 | 15 |
| 19 "github.com/luci/luci-go/client/internal/retry" | 16 "github.com/luci/luci-go/client/internal/retry" |
| 20 ) | 17 ) |
| 21 | 18 |
| 22 // Handler is called once or multiple times for each HTTP request that is tried. | 19 // Handler is called once or multiple times for each HTTP request that is tried. |
| 23 type Handler func(*http.Response) error | 20 type Handler func(*http.Response) error |
| 24 | 21 |
| 25 // Retriable is a retry.Retriable that exposes the resulting HTTP status | 22 // Retriable is a retry.Retriable that exposes the resulting HTTP status |
| 26 // code. | 23 // code. |
| 27 type Retriable interface { | 24 type Retriable interface { |
| 28 retry.Retriable | 25 retry.Retriable |
| 29 // Returns the HTTP status code of the last request, if set. | 26 // Returns the HTTP status code of the last request, if set. |
| 30 Status() int | 27 Status() int |
| 31 } | 28 } |
| 32 | 29 |
| 30 // RequestGen is a generator function to create a new request. It may be called |
| 31 // multiple times if an operation needs to be retried. The HTTP server is |
| 32 // responsible for closing the Request body, as per http.Request Body method |
| 33 // documentation. |
| 34 type RequestGen func() (*http.Request, error) |
| 35 |
| 33 // NewRequest returns a retriable request. | 36 // NewRequest returns a retriable request. |
| 34 // | 37 // |
| 35 // To enable automatic retry support, the Request.Body, if present, must | 38 // To enable automatic retry support, the Request.Body, if present, must |
| 36 // implement io.Seeker. | 39 // implement io.Seeker. |
| 37 // | 40 // |
| 38 // handler should return retry.Error in case of retriable error, for example if | 41 // handler should return retry.Error in case of retriable error, for example if |
| 39 // a TCP connection is teared off while receiving the content. | 42 // a TCP connection is teared off while receiving the content. |
| 40 func NewRequest(c *http.Client, req *http.Request, handler Handler) (Retriable,
error) { | 43 func NewRequest(c *http.Client, rgen RequestGen, handler Handler) Retriable { |
| 41 » // Handle req.Body if specified. It has to implement io.Seeker. | 44 » return &retriable{ |
| 42 » if req.URL.Scheme != "http" && req.URL.Scheme != "https" { | 45 » » handler: handler, |
| 43 » » return nil, fmt.Errorf("unsupported protocol scheme \"%s\"", req
.URL.Scheme) | 46 » » c: c, |
| 47 » » rgen: rgen, |
| 44 } | 48 } |
| 45 newReq := *req | |
| 46 out := &retriable{ | |
| 47 handler: handler, | |
| 48 c: c, | |
| 49 req: &newReq, | |
| 50 closeBody: req.Body, | |
| 51 } | |
| 52 if req.Body != nil { | |
| 53 ok := false | |
| 54 if out.seekBody, ok = req.Body.(io.Seeker); !ok { | |
| 55 return nil, errors.New("req.Body must implement io.Seeke
r") | |
| 56 } | |
| 57 // Make sure the body is not closed when calling http.Client.Do(
). | |
| 58 out.req.Body = ioutil.NopCloser(req.Body) | |
| 59 } | |
| 60 return out, nil | |
| 61 } | 49 } |
| 62 | 50 |
| 63 // NewRequestJSON returns a retriable request calling a JSON endpoint. | 51 // NewRequestJSON returns a retriable request calling a JSON endpoint. |
| 64 func NewRequestJSON(c *http.Client, url, method string, headers map[string]strin
g, in, out interface{}) (Retriable, error) { | 52 func NewRequestJSON(c *http.Client, url, method string, headers map[string]strin
g, in, out interface{}) (Retriable, error) { |
| 65 » var body io.Reader | 53 » var encoded []byte |
| 66 if in != nil { | 54 if in != nil { |
| 67 » » encoded, err := json.Marshal(in) | 55 » » var err error |
| 56 » » if encoded, err = json.Marshal(in); err != nil { |
| 57 » » » return nil, err |
| 58 » » } |
| 59 » } |
| 60 |
| 61 » return NewRequest(c, func() (*http.Request, error) { |
| 62 » » var body io.Reader |
| 63 » » if encoded != nil { |
| 64 » » » body = bytes.NewReader(encoded) |
| 65 » » } |
| 66 |
| 67 » » req, err := http.NewRequest(method, url, body) |
| 68 if err != nil { | 68 if err != nil { |
| 69 return nil, err | 69 return nil, err |
| 70 } | 70 } |
| 71 » » body = newReader(encoded) | 71 » » if encoded != nil { |
| 72 » } | 72 » » » req.Header.Set("Content-Type", jsonContentType) |
| 73 » req, err := http.NewRequest(method, url, body) | |
| 74 » if err != nil { | |
| 75 » » return nil, err | |
| 76 » } | |
| 77 » if in != nil { | |
| 78 » » req.Header.Set("Content-Type", jsonContentType) | |
| 79 » } | |
| 80 » if headers != nil { | |
| 81 » » for k, v := range headers { | |
| 82 » » » req.Header.Add(k, v) | |
| 83 } | 73 } |
| 84 » } | 74 » » if headers != nil { |
| 85 » return NewRequest(c, req, func(resp *http.Response) error { | 75 » » » for k, v := range headers { |
| 76 » » » » req.Header.Add(k, v) |
| 77 » » » } |
| 78 » » } |
| 79 » » return req, nil |
| 80 » }, func(resp *http.Response) error { |
| 86 defer resp.Body.Close() | 81 defer resp.Body.Close() |
| 87 if ct := strings.ToLower(resp.Header.Get("Content-Type")); ct !=
jsonContentType { | 82 if ct := strings.ToLower(resp.Header.Get("Content-Type")); ct !=
jsonContentType { |
| 88 // Non-retriable. | 83 // Non-retriable. |
| 89 return fmt.Errorf("unexpected Content-Type, expected \"%
s\", got \"%s\"", jsonContentType, ct) | 84 return fmt.Errorf("unexpected Content-Type, expected \"%
s\", got \"%s\"", jsonContentType, ct) |
| 90 } | 85 } |
| 91 if out == nil { | 86 if out == nil { |
| 92 // The client doesn't care about the response. Still ens
ure the response | 87 // The client doesn't care about the response. Still ens
ure the response |
| 93 // is valid json. | 88 // is valid json. |
| 94 out = &map[string]interface{}{} | 89 out = &map[string]interface{}{} |
| 95 } | 90 } |
| 96 if err := json.NewDecoder(resp.Body).Decode(out); err != nil { | 91 if err := json.NewDecoder(resp.Body).Decode(out); err != nil { |
| 97 // Retriable. | 92 // Retriable. |
| 98 return retry.Error{fmt.Errorf("bad response %s: %s", url
, err)} | 93 return retry.Error{fmt.Errorf("bad response %s: %s", url
, err)} |
| 99 } | 94 } |
| 100 return nil | 95 return nil |
| 101 » }) | 96 » }), nil |
| 102 } | 97 } |
| 103 | 98 |
| 104 // GetJSON is a shorthand. It returns the HTTP status code and error if any. | 99 // GetJSON is a shorthand. It returns the HTTP status code and error if any. |
| 105 func GetJSON(config *retry.Config, c *http.Client, url string, out interface{})
(int, error) { | 100 func GetJSON(config *retry.Config, c *http.Client, url string, out interface{})
(int, error) { |
| 106 req, err := NewRequestJSON(c, url, "GET", nil, nil, out) | 101 req, err := NewRequestJSON(c, url, "GET", nil, nil, out) |
| 107 if err != nil { | 102 if err != nil { |
| 108 return 0, err | 103 return 0, err |
| 109 } | 104 } |
| 110 err = config.Do(req) | 105 err = config.Do(req) |
| 111 return req.Status(), err | 106 return req.Status(), err |
| 112 } | 107 } |
| 113 | 108 |
| 114 // PostJSON is a shorthand. It returns the HTTP status code and error if any. | 109 // PostJSON is a shorthand. It returns the HTTP status code and error if any. |
| 115 func PostJSON(config *retry.Config, c *http.Client, url string, headers map[stri
ng]string, in, out interface{}) (int, error) { | 110 func PostJSON(config *retry.Config, c *http.Client, url string, headers map[stri
ng]string, in, out interface{}) (int, error) { |
| 116 req, err := NewRequestJSON(c, url, "POST", headers, in, out) | 111 req, err := NewRequestJSON(c, url, "POST", headers, in, out) |
| 117 if err != nil { | 112 if err != nil { |
| 118 return 0, err | 113 return 0, err |
| 119 } | 114 } |
| 120 err = config.Do(req) | 115 err = config.Do(req) |
| 121 return req.Status(), err | 116 return req.Status(), err |
| 122 } | 117 } |
| 123 | 118 |
| 124 // Private details. | 119 // Private details. |
| 125 | 120 |
| 126 const jsonContentType = "application/json; charset=utf-8" | 121 const jsonContentType = "application/json; charset=utf-8" |
| 127 | 122 |
| 128 // newReader returns a io.ReadCloser compatible read-only buffer that also | 123 type retriable struct { |
| 129 // exposes io.Seeker. This should be used instead of bytes.NewReader(), which | 124 » handler Handler |
| 130 // doesn't implement Close(). | 125 » c *http.Client |
| 131 func newReader(p []byte) io.ReadCloser { | 126 » rgen RequestGen |
| 132 » return &reader{bytes.NewReader(p)} | 127 » try int |
| 128 » status int |
| 133 } | 129 } |
| 134 | 130 |
| 135 type reader struct { | 131 func (r *retriable) Close() error { return nil } |
| 136 » *bytes.Reader | |
| 137 } | |
| 138 | |
| 139 func (r *reader) Close() error { | |
| 140 » return nil | |
| 141 } | |
| 142 | |
| 143 type retriable struct { | |
| 144 » handler Handler | |
| 145 » c *http.Client | |
| 146 » req *http.Request | |
| 147 » closeBody io.Closer | |
| 148 » seekBody io.Seeker | |
| 149 » try int | |
| 150 » status int | |
| 151 } | |
| 152 | |
| 153 func (r *retriable) Close() error { | |
| 154 » if r.closeBody != nil { | |
| 155 » » return r.closeBody.Close() | |
| 156 » } | |
| 157 » return nil | |
| 158 } | |
| 159 | 132 |
| 160 // Warning: it returns an error on HTTP >=400. This is different than | 133 // Warning: it returns an error on HTTP >=400. This is different than |
| 161 // http.Client.Do() but hell it makes coding simpler. | 134 // http.Client.Do() but hell it makes coding simpler. |
| 162 func (r *retriable) Do() error { | 135 func (r *retriable) Do() error { |
| 163 » //log.Printf("Do %s", r.req.URL) | 136 » req, err := r.rgen() |
| 164 » if r.seekBody != nil { | 137 » if err != nil { |
| 165 » » // Only do this on retry. | 138 » » return err |
| 166 » » if r.try != 0 { | |
| 167 » » » if _, err := r.seekBody.Seek(0, os.SEEK_SET); err != nil
{ | |
| 168 » » » » // Can't be retried. | |
| 169 » » » » return err | |
| 170 » » » } | |
| 171 » » } | |
| 172 } | 139 } |
| 140 |
| 173 r.try++ | 141 r.try++ |
| 174 » resp, err := r.c.Do(r.req) | 142 » resp, err := r.c.Do(req) |
| 175 if resp != nil { | 143 if resp != nil { |
| 176 r.status = resp.StatusCode | 144 r.status = resp.StatusCode |
| 177 } else { | 145 } else { |
| 178 r.status = 0 | 146 r.status = 0 |
| 179 } | 147 } |
| 180 if err != nil { | 148 if err != nil { |
| 181 // Retry every error. This is sad when you specify an invalid ho
stname but | 149 // Retry every error. This is sad when you specify an invalid ho
stname but |
| 182 // it's better than failing when DNS resolution is flaky. | 150 // it's better than failing when DNS resolution is flaky. |
| 183 return retry.Error{err} | 151 return retry.Error{err} |
| 184 } | 152 } |
| 185 // If the HTTP status code means the request should be retried. | 153 // If the HTTP status code means the request should be retried. |
| 186 if resp.StatusCode == 408 || resp.StatusCode == 429 || resp.StatusCode >
= 500 { | 154 if resp.StatusCode == 408 || resp.StatusCode == 429 || resp.StatusCode >
= 500 { |
| 187 return retry.Error{fmt.Errorf("http request failed: %s (HTTP %d)
", http.StatusText(resp.StatusCode), resp.StatusCode)} | 155 return retry.Error{fmt.Errorf("http request failed: %s (HTTP %d)
", http.StatusText(resp.StatusCode), resp.StatusCode)} |
| 188 } | 156 } |
| 189 // Endpoints occasionally return 404 on valid requests (!) | 157 // Endpoints occasionally return 404 on valid requests (!) |
| 190 » if resp.StatusCode == 404 && strings.HasPrefix(r.req.URL.Path, "/_ah/api
/") { | 158 » if resp.StatusCode == 404 && strings.HasPrefix(req.URL.Path, "/_ah/api/"
) { |
| 191 log.Printf("lhttp.Do() got a Cloud Endpoints 404: %#v", resp.Hea
der) | 159 log.Printf("lhttp.Do() got a Cloud Endpoints 404: %#v", resp.Hea
der) |
| 192 return retry.Error{fmt.Errorf("http request failed (endpoints):
%s (HTTP %d)", http.StatusText(resp.StatusCode), resp.StatusCode)} | 160 return retry.Error{fmt.Errorf("http request failed (endpoints):
%s (HTTP %d)", http.StatusText(resp.StatusCode), resp.StatusCode)} |
| 193 } | 161 } |
| 194 // Any other failure code is a hard failure. | 162 // Any other failure code is a hard failure. |
| 195 if resp.StatusCode >= 400 { | 163 if resp.StatusCode >= 400 { |
| 196 return fmt.Errorf("http request failed: %s (HTTP %d)", http.Stat
usText(resp.StatusCode), resp.StatusCode) | 164 return fmt.Errorf("http request failed: %s (HTTP %d)", http.Stat
usText(resp.StatusCode), resp.StatusCode) |
| 197 } | 165 } |
| 198 // The handler may still return a retry.Error to indicate that the reque
st | 166 // The handler may still return a retry.Error to indicate that the reque
st |
| 199 // should be retried even on successful status code. | 167 // should be retried even on successful status code. |
| 200 return r.handler(resp) | 168 return r.handler(resp) |
| 201 } | 169 } |
| 202 | 170 |
| 203 func (r *retriable) Status() int { | 171 func (r *retriable) Status() int { |
| 204 return r.status | 172 return r.status |
| 205 } | 173 } |
| OLD | NEW |