| 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
|
| +}
|
|
|