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

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

Issue 1159563002: Move retry and lhttp packages from client/internal to common/. (Closed) Base URL: git@github.com:luci/luci-go@master
Patch Set: move-retry-http Created 5 years, 7 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
« no previous file with comments | « client/cmd/swarming/common.go ('k') | client/internal/lhttp/client_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 // found in the LICENSE file.
4
5 package lhttp
6
7 import (
8 "bytes"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io"
13 "io/ioutil"
14 "net/http"
15 "net/url"
16 "os"
17 "strings"
18
19 "github.com/luci/luci-go/client/internal/retry"
20 )
21
22 // Handler is called once or multiple times for each HTTP request that is tried.
23 type Handler func(*http.Response) error
24
25 // Retriable is a retry.Retriable that exposes the resulting HTTP status
26 // code.
27 type Retriable interface {
28 retry.Retriable
29 // Returns the HTTP status code of the last request, if set.
30 Status() int
31 }
32
33 // NewRequest returns a retriable request.
34 //
35 // To enable automatic retry support, the Request.Body, if present, must
36 // implement io.Seeker.
37 //
38 // handler should return retry.Error in case of retriable error, for example if
39 // a TCP connection is teared off while receiving the content.
40 func NewRequest(c *http.Client, req *http.Request, handler Handler) (Retriable, error) {
41 // Handle req.Body if specified. It has to implement io.Seeker.
42 if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
43 return nil, fmt.Errorf("unsupported protocol scheme \"%s\"", req .URL.Scheme)
44 }
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 }
62
63 // NewRequestJSON returns a retriable request calling a JSON endpoint.
64 func NewRequestJSON(c *http.Client, url, method string, in, out interface{}) (Re triable, error) {
65 var body io.Reader
66 if in != nil {
67 encoded, err := json.Marshal(in)
68 if err != nil {
69 return nil, err
70 }
71 body = newReader(encoded)
72 }
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 return NewRequest(c, req, func(resp *http.Response) error {
81 defer resp.Body.Close()
82 if ct := strings.ToLower(resp.Header.Get("Content-Type")); ct != jsonContentType {
83 // Non-retriable.
84 return fmt.Errorf("unexpected Content-Type, expected \"% s\", got \"%s\"", jsonContentType, ct)
85 }
86 if out == nil {
87 // The client doesn't care about the response. Still ens ure the response
88 // is valid json.
89 out = &map[string]interface{}{}
90 }
91 if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
92 // Retriable.
93 return retry.Error{fmt.Errorf("bad response %s: %s", url , err)}
94 }
95 return nil
96 })
97 }
98
99 // GetJSON is a shorthand. It returns the HTTP status code and error if any.
100 func GetJSON(config *retry.Config, c *http.Client, url string, out interface{}) (int, error) {
101 req, err := NewRequestJSON(c, url, "GET", nil, out)
102 if err != nil {
103 return 0, err
104 }
105 err = config.Do(req)
106 return req.Status(), err
107 }
108
109 // PostJSON is a shorthand. It returns the HTTP status code and error if any.
110 func PostJSON(config *retry.Config, c *http.Client, url string, in, out interfac e{}) (int, error) {
111 req, err := NewRequestJSON(c, url, "POST", in, out)
112 if err != nil {
113 return 0, err
114 }
115 err = config.Do(req)
116 return req.Status(), err
117 }
118
119 // Private details.
120
121 const jsonContentType = "application/json; charset=utf-8"
122
123 // newReader returns a io.ReadCloser compatible read-only buffer that also
124 // exposes io.Seeker. This should be used instead of bytes.NewReader(), which
125 // doesn't implement Close().
126 func newReader(p []byte) io.ReadCloser {
127 return &reader{bytes.NewReader(p)}
128 }
129
130 type reader struct {
131 *bytes.Reader
132 }
133
134 func (r *reader) Close() error {
135 return nil
136 }
137
138 type retriable struct {
139 handler Handler
140 c *http.Client
141 req *http.Request
142 closeBody io.Closer
143 seekBody io.Seeker
144 status int
145 }
146
147 func (r *retriable) Close() error {
148 if r.closeBody != nil {
149 return r.closeBody.Close()
150 }
151 return nil
152 }
153
154 // Warning: it returns an error on HTTP >=400. This is different than
155 // http.Client.Do() but hell it makes coding simpler.
156 func (r *retriable) Do() error {
157 if r.seekBody != nil {
158 if _, err := r.seekBody.Seek(0, os.SEEK_SET); err != nil {
159 // Can't be retried.
160 return err
161 }
162 }
163 resp, err := r.c.Do(r.req)
164 if resp != nil {
165 r.status = resp.StatusCode
166 } else {
167 r.status = 0
168 }
169 if err != nil {
170 // Any TCP level failure can be retried but malformed URL should nt.
171 if err2, ok := err.(*url.Error); ok {
172 return err2
173 }
174 return retry.Error{err}
175 }
176 // If the HTTP status code means the request should be retried.
177 if resp.StatusCode == 408 || resp.StatusCode == 429 || resp.StatusCode > = 500 {
178 return retry.Error{fmt.Errorf("http request failed: %s (HTTP %d) ", http.StatusText(resp.StatusCode), resp.StatusCode)}
179 }
180 // Any other failure code is a hard failure.
181 if resp.StatusCode >= 400 {
182 return fmt.Errorf("http request failed: %s (HTTP %d)", http.Stat usText(resp.StatusCode), resp.StatusCode)
183 }
184 // The handler may still return a retry.Error to indicate that the reque st
185 // should be retried even on successful status code.
186 return r.handler(resp)
187 }
188
189 func (r *retriable) Status() int {
190 return r.status
191 }
OLDNEW
« no previous file with comments | « client/cmd/swarming/common.go ('k') | client/internal/lhttp/client_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698