github.com/diamondburned/arikawa@v1.3.14/utils/httputil/client.go (about)

     1  // Package httputil provides abstractions around the common needs of HTTP. It
     2  // also allows swapping in and out the HTTP client.
     3  package httputil
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"io"
     9  	"mime/multipart"
    10  
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
    14  	"github.com/diamondburned/arikawa/utils/json"
    15  )
    16  
    17  // StatusTooManyRequests is the HTTP status code discord sends on rate-limiting.
    18  const StatusTooManyRequests = 429
    19  
    20  // Retries is the default attempts to retry if the API returns an error before
    21  // giving up. If the value is smaller than 1, then requests will retry forever.
    22  var Retries uint = 5
    23  
    24  type Client struct {
    25  	httpdriver.Client
    26  	SchemaEncoder
    27  
    28  	// OnRequest, if not nil, will be copied and prefixed on each Request.
    29  	OnRequest []RequestOption
    30  
    31  	// OnResponse is called after every Do() call. Response might be nil if Do()
    32  	// errors out. The error returned will override Do's if it's not nil.
    33  	OnResponse []ResponseFunc
    34  
    35  	// Default to the global Retries variable (5).
    36  	Retries uint
    37  
    38  	context context.Context
    39  }
    40  
    41  func NewClient() *Client {
    42  	return &Client{
    43  		Client:        httpdriver.NewClient(),
    44  		SchemaEncoder: &DefaultSchema{},
    45  		Retries:       Retries,
    46  		context:       context.Background(),
    47  	}
    48  }
    49  
    50  // Copy returns a shallow copy of the client.
    51  func (c *Client) Copy() *Client {
    52  	cl := new(Client)
    53  	*cl = *c
    54  	return cl
    55  }
    56  
    57  // WithContext returns a client copy of the client with the given context.
    58  func (c *Client) WithContext(ctx context.Context) *Client {
    59  	c = c.Copy()
    60  	c.context = ctx
    61  	return c
    62  }
    63  
    64  // Context is a shared context for all future calls. It's Background by
    65  // default.
    66  func (c *Client) Context() context.Context {
    67  	return c.context
    68  }
    69  
    70  // applyOptions tries to apply all options. It does not halt if a single option
    71  // fails, and the error returned is the latest error.
    72  func (c *Client) applyOptions(r httpdriver.Request, extra []RequestOption) (e error) {
    73  	for _, opt := range c.OnRequest {
    74  		if err := opt(r); err != nil {
    75  			e = err
    76  		}
    77  	}
    78  
    79  	for _, opt := range extra {
    80  		if err := opt(r); err != nil {
    81  			e = err
    82  		}
    83  	}
    84  
    85  	return
    86  }
    87  
    88  func (c *Client) MeanwhileMultipart(
    89  	writer func(*multipart.Writer) error,
    90  	method, url string, opts ...RequestOption) (httpdriver.Response, error) {
    91  
    92  	// We want to cancel the request if our bodyWriter fails.
    93  	ctx, cancel := context.WithCancel(c.context)
    94  	defer cancel()
    95  
    96  	r, w := io.Pipe()
    97  	body := multipart.NewWriter(w)
    98  
    99  	var bgErr error
   100  
   101  	go func() {
   102  		if err := writer(body); err != nil {
   103  			bgErr = err
   104  			cancel()
   105  		}
   106  
   107  		// Close the writer so the body gets flushed to the HTTP reader.
   108  		w.Close()
   109  	}()
   110  
   111  	// Prepend the multipart writer and the correct Content-Type header options.
   112  	opts = PrependOptions(
   113  		opts,
   114  		WithBody(r),
   115  		WithContentType(body.FormDataContentType()),
   116  	)
   117  
   118  	// Request with the current client and our own context:
   119  	resp, err := c.WithContext(ctx).Request(method, url, opts...)
   120  	if err != nil && bgErr != nil {
   121  		return nil, bgErr
   122  	}
   123  	return resp, err
   124  }
   125  
   126  func (c *Client) FastRequest(method, url string, opts ...RequestOption) error {
   127  	r, err := c.Request(method, url, opts...)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	return r.GetBody().Close()
   133  }
   134  
   135  func (c *Client) RequestJSON(to interface{}, method, url string, opts ...RequestOption) error {
   136  	opts = PrependOptions(opts, JSONRequest)
   137  
   138  	r, err := c.Request(method, url, opts...)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	var body, status = r.GetBody(), r.GetStatus()
   144  	defer body.Close()
   145  
   146  	// No content, working as intended (tm)
   147  	if status == httpdriver.NoContent {
   148  		return nil
   149  	}
   150  
   151  	if err := json.DecodeStream(body, to); err != nil {
   152  		return JSONError{err}
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func (c *Client) Request(method, url string, opts ...RequestOption) (httpdriver.Response, error) {
   159  	var doErr error
   160  
   161  	var r httpdriver.Response
   162  	var status int
   163  
   164  	// The c.Retries < 1 check ensures that we retry forever if that field is
   165  	// less than 1.
   166  	for i := uint(0); c.Retries < 1 || i < c.Retries; i++ {
   167  		q, err := c.Client.NewRequest(c.context, method, url)
   168  		if err != nil {
   169  			return nil, RequestError{err}
   170  		}
   171  
   172  		if err := c.applyOptions(q, opts); err != nil {
   173  			// We failed to apply an option, so we should call all OnResponse
   174  			// handler to clean everything up.
   175  			for _, fn := range c.OnResponse {
   176  				fn(q, nil)
   177  			}
   178  			// Exit after cleaning everything up.
   179  			return nil, errors.Wrap(err, "failed to apply options")
   180  		}
   181  
   182  		r, doErr = c.Client.Do(q)
   183  
   184  		// Error that represents the latest error in the chain.
   185  		var onRespErr error
   186  
   187  		// Call OnResponse() even if the request failed.
   188  		for _, fn := range c.OnResponse {
   189  			// Be sure to call ALL OnResponse handlers.
   190  			if err := fn(q, r); err != nil {
   191  				onRespErr = err
   192  			}
   193  		}
   194  
   195  		if onRespErr != nil {
   196  			return nil, errors.Wrap(err, "OnResponse handler failed")
   197  		}
   198  
   199  		// Retry if the request failed.
   200  		if doErr != nil {
   201  			continue
   202  		}
   203  
   204  		if status = r.GetStatus(); status == StatusTooManyRequests || status >= 500 {
   205  			continue
   206  		}
   207  
   208  		break
   209  	}
   210  
   211  	// If all retries failed:
   212  	if doErr != nil {
   213  		return nil, RequestError{doErr}
   214  	}
   215  
   216  	// Response received, but with a failure status code:
   217  	if status < 200 || status > 299 {
   218  		// Try and parse the body.
   219  		var body = r.GetBody()
   220  		defer body.Close()
   221  
   222  		// This rarely happens, so we can (probably) make an exception for it.
   223  		buf := bytes.Buffer{}
   224  		buf.ReadFrom(body)
   225  
   226  		httpErr := &HTTPError{
   227  			Status: status,
   228  			Body:   buf.Bytes(),
   229  		}
   230  
   231  		// Optionally unmarshal the error.
   232  		json.Unmarshal(httpErr.Body, &httpErr)
   233  
   234  		return nil, httpErr
   235  	}
   236  
   237  	return r, nil
   238  }