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

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

Issue 1135173003: Create packages client/internal/ retry and lhttp. (Closed) Base URL: git@github.com:luci/luci-go@3_UI
Patch Set: . 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
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 // NewRequest returns a retriable request.
26 //
27 // To enable automatic retry support, the Request.Body, if present, must
28 // implement io.Seeker.
29 //
30 // handler should return retry.Error in case of retriable error, for example if
31 // a TCP connection is teared off while receiving the content.
32 func NewRequest(c *http.Client, req *http.Request, handler Handler) (retry.Retri able, error) {
33 // Handle req.Body if specified. It has to implement io.Seeker.
34 newReq := *req
35 out := &httpRetriable{
36 handler: handler,
37 c: c,
38 req: &newReq,
39 closeBody: req.Body,
40 }
41 if req.Body != nil {
42 ok := false
43 if out.seekBody, ok = req.Body.(io.Seeker); !ok {
44 return nil, errors.New("req.Body must implement io.Seeke r")
45 }
46 // Make sure the body is not closed when calling http.Client.Do( ).
47 out.req.Body = ioutil.NopCloser(req.Body)
48 }
49 return out, nil
50 }
51
52 // NewRequestJSON returns a retriable request calling a JSON endpoint.
53 func NewRequestJSON(c *http.Client, url, method string, in, out interface{}) (re try.Retriable, error) {
54 var body io.Reader
55 if in != nil {
56 encoded, err := json.Marshal(in)
57 if err != nil {
58 return nil, err
59 }
60 body = NewReader(encoded)
61 }
62 req, err := http.NewRequest(method, url, body)
63 if err != nil {
64 return nil, err
65 }
66 if in != nil {
67 req.Header.Set("Content-Type", jsonContentType)
68 }
69 return NewRequest(c, req, func(resp *http.Response) error {
70 defer resp.Body.Close()
71 if ct := strings.ToLower(resp.Header.Get("Content-Type")); ct != jsonContentType {
72 // Non-retriable.
73 return fmt.Errorf("unexpected Content-Type, expected \"% s\", got \"%s\"", jsonContentType, ct)
74 }
75 if out == nil {
76 // The client doesn't care about the response. Still ens ure the response
77 // is valid json.
78 out = &map[string]interface{}{}
79 }
80 if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
81 // 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
82 return retry.Error{fmt.Errorf("bad response %s: %s", url , err)}
83 }
84 return nil
85 })
86 }
87
88 // GetJSON is a shorthand.
89 func GetJSON(config *retry.Config, c *http.Client, url string, out interface{}) error {
90 req, err := NewRequestJSON(c, url, "GET", nil, out)
91 if err != nil {
92 return err
93 }
94 return config.Do(req)
95 }
96
97 // PostJSON is a shorthand.
98 func PostJSON(config *retry.Config, c *http.Client, url string, in, out interfac e{}) error {
99 req, err := NewRequestJSON(c, url, "POST", in, out)
100 if err != nil {
101 return err
102 }
103 return config.Do(req)
104 }
105
106 // 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
107 // exposes io.Seeker. This should be used instead of bytes.NewReader(), which
108 // doesn't implement Close().
109 func NewReader(p []byte) io.ReadCloser {
110 return &reader{bytes.NewReader(p)}
111 }
112
113 // Private details.
114
115 const jsonContentType = "application/json; charset=utf-8"
116
117 type reader struct {
118 *bytes.Reader
119 }
120
121 func (r *reader) Close() error {
122 return nil
123 }
124
125 type httpRetriable struct {
126 handler Handler
127 c *http.Client
128 req *http.Request
129 closeBody io.Closer
130 seekBody io.Seeker
131 }
132
133 func (h *httpRetriable) Close() error {
134 if h.closeBody != nil {
135 return h.closeBody.Close()
136 }
137 return nil
138 }
139
140 // Warning: it returns an error on HTTP >=400. This is different than
141 // http.Client.Do() but hell it makes coding simpler.
142 func (h *httpRetriable) Do() error {
143 if h.seekBody != nil {
144 if _, err := h.seekBody.Seek(0, os.SEEK_SET); err != nil {
145 // Can't be retried.
146 return err
147 }
148 }
149 resp, err := h.c.Do(h.req)
150 if err != nil {
151 // Any TCP level failure can be retried but malformed URL should nt.
152 if err2, ok := err.(*url.Error); ok {
153 return err2
154 }
155 // http.badStringError{} is not exported. Hack around.
156 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
157 return err
158 }
159 return retry.Error{err}
160 }
161 // If the HTTP status code means the request should be retried.
162 if resp.StatusCode == 408 || resp.StatusCode == 429 || resp.StatusCode > = 500 {
163 return retry.Error{fmt.Errorf("http request failed: %s (HTTP %d) ", http.StatusText(resp.StatusCode), resp.StatusCode)}
164 }
165 // Any other failure code is a hard failure.
166 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
167 return fmt.Errorf("http request failed: %s (HTTP %d)", http.Stat usText(resp.StatusCode), resp.StatusCode)
168 }
169 // The handler may still return a retry.Error to indicate that the reque st
170 // should be retried even on successful status code.
171 return h.handler(resp)
172 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698