Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(32)

Side by Side Diff: client/internal/lhttp/client.go

Issue 1846263002: Isolate: Use generators instead of seekers (Closed) Base URL: https://github.com/luci/luci-go@master
Patch Set: Tweaks from comments. Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698