github.com/diamondburned/arikawa/v2@v2.1.0/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  	"time"
    11  
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/diamondburned/arikawa/v2/utils/httputil/httpdriver"
    15  	"github.com/diamondburned/arikawa/v2/utils/json"
    16  )
    17  
    18  // StatusTooManyRequests is the HTTP status code discord sends on rate-limiting.
    19  const StatusTooManyRequests = 429
    20  
    21  // Retries is the default attempts to retry if the API returns an error before
    22  // giving up. If the value is smaller than 1, then requests will retry forever.
    23  var Retries uint = 5
    24  
    25  type Client struct {
    26  	httpdriver.Client
    27  	SchemaEncoder
    28  
    29  	// OnRequest, if not nil, will be copied and prefixed on each Request.
    30  	OnRequest []RequestOption
    31  
    32  	// OnResponse is called after every Do() call. Response might be nil if Do()
    33  	// errors out. The error returned will override Do's if it's not nil.
    34  	OnResponse []ResponseFunc
    35  
    36  	// Timeout is the maximum amount of time the client will wait for a request
    37  	// to finish. If this is 0 or smaller the Client won't time out. Otherwise,
    38  	// the timeout will be used as deadline for context of every request.
    39  	Timeout time.Duration
    40  
    41  	// Default to the global Retries variable (5).
    42  	Retries uint
    43  
    44  	context context.Context
    45  }
    46  
    47  func NewClient() *Client {
    48  	return &Client{
    49  		Client:        httpdriver.NewClient(),
    50  		SchemaEncoder: &DefaultSchema{},
    51  		Retries:       Retries,
    52  		context:       context.Background(),
    53  	}
    54  }
    55  
    56  // Copy returns a shallow copy of the client.
    57  func (c *Client) Copy() *Client {
    58  	cl := new(Client)
    59  	*cl = *c
    60  	return cl
    61  }
    62  
    63  // WithContext returns a client copy of the client with the given context.
    64  func (c *Client) WithContext(ctx context.Context) *Client {
    65  	c = c.Copy()
    66  	c.context = ctx
    67  	return c
    68  }
    69  
    70  // Context is a shared context for all future calls. It's Background by
    71  // default.
    72  func (c *Client) Context() context.Context {
    73  	return c.context
    74  }
    75  
    76  // applyOptions tries to apply all options. It does not halt if a single option
    77  // fails, and the error returned is the latest error.
    78  func (c *Client) applyOptions(r httpdriver.Request, extra []RequestOption) (e error) {
    79  	for _, opt := range c.OnRequest {
    80  		if err := opt(r); err != nil {
    81  			e = err
    82  		}
    83  	}
    84  
    85  	for _, opt := range extra {
    86  		if err := opt(r); err != nil {
    87  			e = err
    88  		}
    89  	}
    90  
    91  	return
    92  }
    93  
    94  // MultipartWriter is the interface for a data structure that can write into a
    95  // multipart writer.
    96  type MultipartWriter interface {
    97  	WriteMultipart(body *multipart.Writer) error
    98  }
    99  
   100  // MeanwhileMultipart concurrently encodes and writes the given multipart writer
   101  // at the same time. The writer will be called in another goroutine, but the
   102  // writer will be closed when MeanwhileMultipart returns.
   103  func (c *Client) MeanwhileMultipart(
   104  	writer MultipartWriter,
   105  	method, url string, opts ...RequestOption) (httpdriver.Response, error) {
   106  
   107  	r, w := io.Pipe()
   108  	body := multipart.NewWriter(w)
   109  
   110  	// Ensure the writer is closed by the time this function exits, so
   111  	// WriteMultipart will exit.
   112  	defer w.Close()
   113  
   114  	go func() {
   115  		err := writer.WriteMultipart(body)
   116  		body.Close()
   117  		w.CloseWithError(err)
   118  	}()
   119  
   120  	// Prepend the multipart writer and the correct Content-Type header options.
   121  	opts = PrependOptions(
   122  		opts,
   123  		WithBody(r),
   124  		WithContentType(body.FormDataContentType()),
   125  	)
   126  
   127  	// Request with the current client and our own context:
   128  	return c.Request(method, url, opts...)
   129  }
   130  
   131  // FastRequest performs a request without waiting for the body.
   132  func (c *Client) FastRequest(method, url string, opts ...RequestOption) error {
   133  	r, err := c.Request(method, url, opts...)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	return r.GetBody().Close()
   139  }
   140  
   141  // RequestJSON performs a request and unmarshals the JSON body into "to".
   142  func (c *Client) RequestJSON(to interface{}, method, url string, opts ...RequestOption) error {
   143  	opts = PrependOptions(opts, JSONRequest)
   144  
   145  	r, err := c.Request(method, url, opts...)
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	var body, status = r.GetBody(), r.GetStatus()
   151  	defer body.Close()
   152  
   153  	// No content, working as intended (tm)
   154  	if status == httpdriver.NoContent {
   155  		return nil
   156  	}
   157  	// to is nil for some reason. Ignore.
   158  	if to == nil {
   159  		return nil
   160  	}
   161  
   162  	if err := json.DecodeStream(body, to); err != nil {
   163  		return JSONError{err}
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  // Request performs a request and returns a response with an unread body. The
   170  // caller must close it manually.
   171  func (c *Client) Request(method, url string, opts ...RequestOption) (httpdriver.Response, error) {
   172  	response, cancel, err := c.request(method, url, opts)
   173  	if err != nil {
   174  		if cancel != nil {
   175  			cancel()
   176  		}
   177  		return nil, err
   178  	}
   179  
   180  	if cancel != nil {
   181  		return wrapCancelableResponse(response, cancel), nil
   182  	}
   183  
   184  	return response, nil
   185  }
   186  
   187  func (c *Client) request(
   188  	method, url string,
   189  	opts []RequestOption) (r httpdriver.Response, cancel context.CancelFunc, doErr error) {
   190  
   191  	// Error that represents the latest error in the chain.
   192  	var onRespErr error
   193  
   194  	var status int
   195  
   196  	ctx := c.context
   197  
   198  	if c.Timeout > 0 {
   199  		ctx, cancel = context.WithTimeout(ctx, c.Timeout)
   200  	}
   201  
   202  	// The c.Retries < 1 check ensures that we retry forever if that field is
   203  	// less than 1.
   204  	for i := uint(0); c.Retries < 1 || i < c.Retries; i++ {
   205  		q, err := c.Client.NewRequest(ctx, method, url)
   206  		if err != nil {
   207  			doErr = RequestError{err}
   208  			return
   209  		}
   210  
   211  		if err := c.applyOptions(q, opts); err != nil {
   212  			// We failed to apply an option, so we should call all OnResponse
   213  			// handler to clean everything up.
   214  			for _, fn := range c.OnResponse {
   215  				fn(q, nil)
   216  			}
   217  
   218  			doErr = errors.Wrap(err, "failed to apply http request options")
   219  			return
   220  		}
   221  
   222  		r, doErr = c.Client.Do(q)
   223  
   224  		// Call OnResponse() even if the request failed.
   225  		for _, fn := range c.OnResponse {
   226  			// Be sure to call ALL OnResponse handlers.
   227  			if err := fn(q, r); err != nil {
   228  				onRespErr = err
   229  			}
   230  		}
   231  
   232  		if onRespErr != nil || doErr != nil {
   233  			continue
   234  		}
   235  
   236  		if status = r.GetStatus(); status == StatusTooManyRequests || status >= 500 {
   237  			continue
   238  		}
   239  
   240  		break
   241  	}
   242  
   243  	if onRespErr != nil {
   244  		doErr = errors.Wrap(onRespErr, "OnResponse handler failed")
   245  		return
   246  	}
   247  
   248  	// If all retries failed, then wrap and return.
   249  	if doErr != nil {
   250  		doErr = RequestError{doErr}
   251  		return
   252  	}
   253  
   254  	// Response received, but with a failure status code:
   255  	if status < 200 || status > 299 {
   256  		// Try and parse the body.
   257  		var body = r.GetBody()
   258  		defer body.Close()
   259  
   260  		// This rarely happens, so we can (probably) make an exception for it.
   261  		buf := bytes.Buffer{}
   262  		buf.ReadFrom(body)
   263  
   264  		httpErr := &HTTPError{
   265  			Status: status,
   266  			Body:   buf.Bytes(),
   267  		}
   268  
   269  		// Optionally unmarshal the error.
   270  		json.Unmarshal(httpErr.Body, &httpErr)
   271  
   272  		doErr = httpErr
   273  	}
   274  
   275  	return
   276  }