| OLD | NEW |
| 1 // Copyright 2015 The LUCI Authors. All rights reserved. | 1 // Copyright 2015 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be 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 "fmt" | 10 "fmt" |
| 11 "io" | 11 "io" |
| 12 "io/ioutil" |
| 12 "net/http" | 13 "net/http" |
| 13 "strings" | 14 "strings" |
| 14 | 15 |
| 15 "golang.org/x/net/context" | 16 "golang.org/x/net/context" |
| 16 | 17 |
| 17 "github.com/luci/luci-go/common/errors" | 18 "github.com/luci/luci-go/common/errors" |
| 18 "github.com/luci/luci-go/common/logging" | 19 "github.com/luci/luci-go/common/logging" |
| 19 "github.com/luci/luci-go/common/retry" | 20 "github.com/luci/luci-go/common/retry" |
| 20 ) | 21 ) |
| 21 | 22 |
| 22 const ( | 23 const ( |
| 23 jsonContentType = "application/json" | 24 jsonContentType = "application/json" |
| 24 jsonContentTypeForPOST = "application/json; charset=utf-8" | 25 jsonContentTypeForPOST = "application/json; charset=utf-8" |
| 25 ) | 26 ) |
| 26 | 27 |
| 27 // Handler is called once or multiple times for each HTTP request that is tried. | 28 // Handler is called once or multiple times for each HTTP request that is tried. |
| 28 type Handler func(*http.Response) error | 29 type Handler func(*http.Response) error |
| 29 | 30 |
| 30 // RequestGen is a generator function to create a new request. It may be called | 31 // 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 // 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 // responsible for closing the Request body, as per http.Request Body method |
| 33 // documentation. | 34 // documentation. |
| 34 type RequestGen func() (*http.Request, error) | 35 type RequestGen func() (*http.Request, error) |
| 35 | 36 |
| 36 // NewRequest returns a retriable request. | 37 // NewRequest returns a retriable request. |
| 37 // | 38 // |
| 38 // To enable automatic retry support, the Request.Body, if present, must | 39 // The handler func is responsible for closing the response Body before |
| 39 // implement io.Seeker. | 40 // returning. It should return retry.Error in case of retriable error, for |
| 40 // | 41 // example if a TCP connection is terminated while receiving the content. |
| 41 // handler should return retry.Error in case of retriable error, for example if | |
| 42 // a TCP connection is teared off while receiving the content. | |
| 43 func NewRequest(ctx context.Context, c *http.Client, rFn retry.Factory, rgen Req
uestGen, handler Handler) func() (int, error) { | 42 func NewRequest(ctx context.Context, c *http.Client, rFn retry.Factory, rgen Req
uestGen, handler Handler) func() (int, error) { |
| 44 if rFn == nil { | 43 if rFn == nil { |
| 45 rFn = retry.Default | 44 rFn = retry.Default |
| 46 } | 45 } |
| 47 | 46 |
| 48 return func() (int, error) { | 47 return func() (int, error) { |
| 49 » » status := 0 | 48 » » status, attempts := 0, 0 |
| 50 err := retry.Retry(ctx, rFn, func() error { | 49 err := retry.Retry(ctx, rFn, func() error { |
| 50 attempts++ |
| 51 req, err := rgen() | 51 req, err := rgen() |
| 52 if err != nil { | 52 if err != nil { |
| 53 return err | 53 return err |
| 54 } | 54 } |
| 55 | 55 |
| 56 resp, err := c.Do(req) | 56 resp, err := c.Do(req) |
| 57 if err != nil { | 57 if err != nil { |
| 58 // Retry every error. This is sad when you speci
fy an invalid hostname but | 58 // Retry every error. This is sad when you speci
fy an invalid hostname but |
| 59 // it's better than failing when DNS resolution
is flaky. | 59 // it's better than failing when DNS resolution
is flaky. |
| 60 return errors.WrapTransient(err) | 60 return errors.WrapTransient(err) |
| 61 } | 61 } |
| 62 status = resp.StatusCode | 62 status = resp.StatusCode |
| 63 | 63 |
| 64 » » » // If the HTTP status code means the request should be r
etried. | 64 » » » switch { |
| 65 » » » if status == 408 || status == 429 || status >= 500 { | 65 » » » case status == 408, status == 429, status >= 500: |
| 66 » » » » return errors.WrapTransient( | 66 » » » » // The HTTP status code means the request should
be retried. |
| 67 » » » » err = errors.WrapTransient( |
| 67 fmt.Errorf("http request failed: %s (HTT
P %d)", http.StatusText(status), status)) | 68 fmt.Errorf("http request failed: %s (HTT
P %d)", http.StatusText(status), status)) |
| 69 case status == 404 && strings.HasPrefix(req.URL.Path, "/
_ah/api/"): |
| 70 // Endpoints occasionally return 404 on valid re
quests! |
| 71 logging.Infof(ctx, "lhttp.Do() got a Cloud Endpo
ints 404: %#v", resp.Header) |
| 72 err = errors.WrapTransient( |
| 73 fmt.Errorf("http request failed (endpoin
ts): %s (HTTP %d)", http.StatusText(status), status)) |
| 74 case status >= 400: |
| 75 // Any other failure code is a hard failure. |
| 76 err = fmt.Errorf("http request failed: %s (HTTP
%d)", http.StatusText(status), status) |
| 77 default: |
| 78 // The handler may still return a retry.Error to
indicate that the request |
| 79 // should be retried even on successful status c
ode. |
| 80 return handler(resp) |
| 68 } | 81 } |
| 69 » » » // Endpoints occasionally return 404 on valid requests (
!) | 82 |
| 70 » » » if status == 404 && strings.HasPrefix(req.URL.Path, "/_a
h/api/") { | 83 » » » // Drain and close the resp.Body. |
| 71 » » » » logging.Infof(ctx, "lhttp.Do() got a Cloud Endpo
ints 404: %#v", resp.Header) | 84 » » » io.Copy(ioutil.Discard, resp.Body) |
| 72 » » » » return errors.WrapTransient( | 85 » » » resp.Body.Close() |
| 73 » » » » » fmt.Errorf("http request failed (endpoin
ts): %s (HTTP %d)", http.StatusText(status), status)) | 86 » » » return err |
| 74 » » » } | |
| 75 » » » // Any other failure code is a hard failure. | |
| 76 » » » if status >= 400 { | |
| 77 » » » » return fmt.Errorf("http request failed: %s (HTTP
%d)", http.StatusText(status), status) | |
| 78 » » » } | |
| 79 » » » // The handler may still return a retry.Error to indicat
e that the request | |
| 80 » » » // should be retried even on successful status code. | |
| 81 » » » return handler(resp) | |
| 82 }, nil) | 87 }, nil) |
| 88 if err != nil { |
| 89 err = fmt.Errorf("%v (attempts: %d)", err, attempts) |
| 90 } |
| 83 return status, err | 91 return status, err |
| 84 } | 92 } |
| 85 } | 93 } |
| 86 | 94 |
| 87 // NewRequestJSON returns a retriable request calling a JSON endpoint. | 95 // NewRequestJSON returns a retriable request calling a JSON endpoint. |
| 88 func NewRequestJSON(ctx context.Context, c *http.Client, rFn retry.Factory, url,
method string, headers map[string]string, in, out interface{}) (func() (int, er
ror), error) { | 96 func NewRequestJSON(ctx context.Context, c *http.Client, rFn retry.Factory, url,
method string, headers map[string]string, in, out interface{}) (func() (int, er
ror), error) { |
| 89 var encoded []byte | 97 var encoded []byte |
| 90 if in != nil { | 98 if in != nil { |
| 91 var err error | 99 var err error |
| 92 if encoded, err = json.Marshal(in); err != nil { | 100 if encoded, err = json.Marshal(in); err != nil { |
| (...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 142 } | 150 } |
| 143 | 151 |
| 144 // PostJSON is a shorthand. It returns the HTTP status code and error if any. | 152 // PostJSON is a shorthand. It returns the HTTP status code and error if any. |
| 145 func PostJSON(ctx context.Context, rFn retry.Factory, c *http.Client, url string
, headers map[string]string, in, out interface{}) (int, error) { | 153 func PostJSON(ctx context.Context, rFn retry.Factory, c *http.Client, url string
, headers map[string]string, in, out interface{}) (int, error) { |
| 146 req, err := NewRequestJSON(ctx, c, rFn, url, "POST", headers, in, out) | 154 req, err := NewRequestJSON(ctx, c, rFn, url, "POST", headers, in, out) |
| 147 if err != nil { | 155 if err != nil { |
| 148 return 0, err | 156 return 0, err |
| 149 } | 157 } |
| 150 return req() | 158 return req() |
| 151 } | 159 } |
| OLD | NEW |