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

Unified 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: Create lhttp.Retriable interface 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « client/internal/common/utils_test.go ('k') | client/internal/lhttp/client_test.go » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: client/internal/lhttp/client.go
diff --git a/client/internal/lhttp/client.go b/client/internal/lhttp/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..c277d18e9705e003f00ebd757db5c71ef79f906a
--- /dev/null
+++ b/client/internal/lhttp/client.go
@@ -0,0 +1,191 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package lhttp
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+
+ "github.com/luci/luci-go/client/internal/retry"
+)
+
+// Handler is called once or multiple times for each HTTP request that is tried.
+type Handler func(*http.Response) error
+
+// Retriable is a retry.Retriable that exposes the resulting HTTP status
+// code.
+type Retriable interface {
+ retry.Retriable
+ // Returns the HTTP status code of the last request, if set.
+ Status() int
+}
+
+// NewRequest returns a retriable request.
+//
+// To enable automatic retry support, the Request.Body, if present, must
+// implement io.Seeker.
+//
+// handler should return retry.Error in case of retriable error, for example if
+// a TCP connection is teared off while receiving the content.
+func NewRequest(c *http.Client, req *http.Request, handler Handler) (Retriable, error) {
+ // Handle req.Body if specified. It has to implement io.Seeker.
+ if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
+ return nil, fmt.Errorf("unsupported protocol scheme \"%s\"", req.URL.Scheme)
+ }
+ newReq := *req
+ out := &retriable{
+ handler: handler,
+ c: c,
+ req: &newReq,
+ closeBody: req.Body,
+ }
+ if req.Body != nil {
+ ok := false
+ if out.seekBody, ok = req.Body.(io.Seeker); !ok {
+ return nil, errors.New("req.Body must implement io.Seeker")
+ }
+ // Make sure the body is not closed when calling http.Client.Do().
+ out.req.Body = ioutil.NopCloser(req.Body)
+ }
+ return out, nil
+}
+
+// NewRequestJSON returns a retriable request calling a JSON endpoint.
+func NewRequestJSON(c *http.Client, url, method string, in, out interface{}) (Retriable, error) {
+ var body io.Reader
+ if in != nil {
+ encoded, err := json.Marshal(in)
+ if err != nil {
+ return nil, err
+ }
+ body = newReader(encoded)
+ }
+ req, err := http.NewRequest(method, url, body)
+ if err != nil {
+ return nil, err
+ }
+ if in != nil {
+ req.Header.Set("Content-Type", jsonContentType)
+ }
+ return NewRequest(c, req, func(resp *http.Response) error {
+ defer resp.Body.Close()
+ if ct := strings.ToLower(resp.Header.Get("Content-Type")); ct != jsonContentType {
+ // Non-retriable.
+ return fmt.Errorf("unexpected Content-Type, expected \"%s\", got \"%s\"", jsonContentType, ct)
+ }
+ if out == nil {
+ // The client doesn't care about the response. Still ensure the response
+ // is valid json.
+ out = &map[string]interface{}{}
+ }
+ if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
+ // Retriable.
+ return retry.Error{fmt.Errorf("bad response %s: %s", url, err)}
+ }
+ return nil
+ })
+}
+
+// GetJSON is a shorthand. It returns the HTTP status code and error if any.
+func GetJSON(config *retry.Config, c *http.Client, url string, out interface{}) (int, error) {
+ req, err := NewRequestJSON(c, url, "GET", nil, out)
+ if err != nil {
+ return 0, err
+ }
+ err = config.Do(req)
+ return req.Status(), err
+}
+
+// PostJSON is a shorthand. It returns the HTTP status code and error if any.
+func PostJSON(config *retry.Config, c *http.Client, url string, in, out interface{}) (int, error) {
+ req, err := NewRequestJSON(c, url, "POST", in, out)
+ if err != nil {
+ return 0, err
+ }
+ err = config.Do(req)
+ return req.Status(), err
+}
+
+// Private details.
+
+const jsonContentType = "application/json; charset=utf-8"
+
+// newReader returns a io.ReadCloser compatible read-only buffer that also
+// exposes io.Seeker. This should be used instead of bytes.NewReader(), which
+// doesn't implement Close().
+func newReader(p []byte) io.ReadCloser {
+ return &reader{bytes.NewReader(p)}
+}
+
+type reader struct {
+ *bytes.Reader
+}
+
+func (r *reader) Close() error {
+ return nil
+}
+
+type retriable struct {
+ handler Handler
+ c *http.Client
+ req *http.Request
+ closeBody io.Closer
+ seekBody io.Seeker
+ status int
+}
+
+func (r *retriable) Close() error {
+ if r.closeBody != nil {
+ return r.closeBody.Close()
+ }
+ return nil
+}
+
+// Warning: it returns an error on HTTP >=400. This is different than
+// http.Client.Do() but hell it makes coding simpler.
+func (r *retriable) Do() error {
+ if r.seekBody != nil {
+ if _, err := r.seekBody.Seek(0, os.SEEK_SET); err != nil {
+ // Can't be retried.
+ return err
+ }
+ }
+ resp, err := r.c.Do(r.req)
+ if resp != nil {
+ r.status = resp.StatusCode
+ } else {
+ r.status = 0
+ }
+ if err != nil {
+ // Any TCP level failure can be retried but malformed URL should nt.
+ if err2, ok := err.(*url.Error); ok {
+ return err2
+ }
+ return retry.Error{err}
+ }
+ // If the HTTP status code means the request should be retried.
+ if resp.StatusCode == 408 || resp.StatusCode == 429 || resp.StatusCode >= 500 {
+ return retry.Error{fmt.Errorf("http request failed: %s (HTTP %d)", http.StatusText(resp.StatusCode), resp.StatusCode)}
+ }
+ // Any other failure code is a hard failure.
+ if resp.StatusCode >= 400 {
+ return fmt.Errorf("http request failed: %s (HTTP %d)", http.StatusText(resp.StatusCode), resp.StatusCode)
+ }
+ // The handler may still return a retry.Error to indicate that the request
+ // should be retried even on successful status code.
+ return r.handler(resp)
+}
+
+func (r *retriable) Status() int {
+ return r.status
+}
« no previous file with comments | « client/internal/common/utils_test.go ('k') | client/internal/lhttp/client_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698