1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-10-07 10:09:08 +00:00
arikawa/utils/httputil/client.go
diamondburned 331ec59dec discord: Refactor interactions and components
This commit gets rid of contain-it-all structs and instead opt for
interface union types containing underlying concrete types with no
overloading.

The code is much more verbose by doing this, but the API is much nicer
to use. The only disadvantage in that regard is the interface assertion
being too verbose and risky for users at times.
2021-11-12 11:38:36 -08:00

277 lines
6.6 KiB
Go

// Package httputil provides abstractions around the common needs of HTTP. It
// also allows swapping in and out the HTTP client.
package httputil
import (
"bytes"
"context"
"io"
"mime/multipart"
"time"
"github.com/pkg/errors"
"github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver"
"github.com/diamondburned/arikawa/v3/utils/json"
)
// StatusTooManyRequests is the HTTP status code discord sends on rate-limiting.
const StatusTooManyRequests = 429
// Retries is the default attempts to retry if the API returns an error before
// giving up. If the value is smaller than 1, then requests will retry forever.
var Retries uint = 5
type Client struct {
httpdriver.Client
SchemaEncoder
// OnRequest, if not nil, will be copied and prefixed on each Request.
OnRequest []RequestOption
// OnResponse is called after every Do() call. Response might be nil if Do()
// errors out. The error returned will override Do's if it's not nil.
OnResponse []ResponseFunc
// Timeout is the maximum amount of time the client will wait for a request
// to finish. If this is 0 or smaller the Client won't time out. Otherwise,
// the timeout will be used as deadline for context of every request.
Timeout time.Duration
// Default to the global Retries variable (5).
Retries uint
context context.Context
}
func NewClient() *Client {
return &Client{
Client: httpdriver.NewClient(),
SchemaEncoder: &DefaultSchema{},
Retries: Retries,
context: context.Background(),
}
}
// Copy returns a shallow copy of the client.
func (c *Client) Copy() *Client {
cl := new(Client)
*cl = *c
return cl
}
// WithContext returns a client copy of the client with the given context.
func (c *Client) WithContext(ctx context.Context) *Client {
c = c.Copy()
c.context = ctx
return c
}
// Context is a shared context for all future calls. It's Background by
// default.
func (c *Client) Context() context.Context {
return c.context
}
// applyOptions tries to apply all options. It does not halt if a single option
// fails, and the error returned is the latest error.
func (c *Client) applyOptions(r httpdriver.Request, extra []RequestOption) (e error) {
for _, opt := range c.OnRequest {
if err := opt(r); err != nil {
e = err
}
}
for _, opt := range extra {
if err := opt(r); err != nil {
e = err
}
}
return
}
// MultipartWriter is the interface for a data structure that can write into a
// multipart writer.
type MultipartWriter interface {
WriteMultipart(body *multipart.Writer) error
}
// MeanwhileMultipart concurrently encodes and writes the given multipart writer
// at the same time. The writer will be called in another goroutine, but the
// writer will be closed when MeanwhileMultipart returns.
func (c *Client) MeanwhileMultipart(
writer MultipartWriter,
method, url string, opts ...RequestOption) (httpdriver.Response, error) {
r, w := io.Pipe()
body := multipart.NewWriter(w)
// Ensure the writer is closed by the time this function exits, so
// WriteMultipart will exit.
defer w.Close()
go func() {
err := writer.WriteMultipart(body)
body.Close()
w.CloseWithError(err)
}()
// Prepend the multipart writer and the correct Content-Type header options.
opts = PrependOptions(
opts,
WithBody(r),
WithContentType(body.FormDataContentType()),
)
// Request with the current client and our own context:
return c.Request(method, url, opts...)
}
// FastRequest performs a request without waiting for the body.
func (c *Client) FastRequest(method, url string, opts ...RequestOption) error {
r, err := c.Request(method, url, opts...)
if err != nil {
return err
}
return r.GetBody().Close()
}
// RequestJSON performs a request and unmarshals the JSON body into "to".
func (c *Client) RequestJSON(to interface{}, method, url string, opts ...RequestOption) error {
opts = PrependOptions(opts, JSONRequest)
r, err := c.Request(method, url, opts...)
if err != nil {
return err
}
var body, status = r.GetBody(), r.GetStatus()
defer body.Close()
// No content, working as intended (tm)
if status == httpdriver.NoContent {
return nil
}
// to is nil for some reason. Ignore.
if to == nil {
return nil
}
if err := json.DecodeStream(body, to); err != nil {
return JSONError{err}
}
return nil
}
// Request performs a request and returns a response with an unread body. The
// caller must close it manually.
func (c *Client) Request(method, url string, opts ...RequestOption) (httpdriver.Response, error) {
response, cancel, err := c.request(method, url, opts)
if err != nil {
if cancel != nil {
cancel()
}
return nil, err
}
if cancel != nil {
return wrapCancelableResponse(response, cancel), nil
}
return response, nil
}
func (c *Client) request(
method, url string,
opts []RequestOption) (r httpdriver.Response, cancel context.CancelFunc, doErr error) {
// Error that represents the latest error in the chain.
var onRespErr error
var status int
ctx := c.context
if c.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, c.Timeout)
}
// The c.Retries < 1 check ensures that we retry forever if that field is
// less than 1.
for i := uint(0); c.Retries < 1 || i < c.Retries; i++ {
q, err := c.Client.NewRequest(ctx, method, url)
if err != nil {
doErr = RequestError{err}
return
}
if err := c.applyOptions(q, opts); err != nil {
// We failed to apply an option, so we should call all OnResponse
// handler to clean everything up.
for _, fn := range c.OnResponse {
fn(q, nil)
}
doErr = errors.Wrap(err, "failed to apply http request options")
return
}
r, doErr = c.Client.Do(q)
// Call OnResponse() even if the request failed.
for _, fn := range c.OnResponse {
// Be sure to call ALL OnResponse handlers.
if err := fn(q, r); err != nil {
onRespErr = err
}
}
if onRespErr != nil || doErr != nil {
continue
}
if status = r.GetStatus(); status == StatusTooManyRequests || status >= 500 {
continue
}
break
}
if onRespErr != nil {
doErr = errors.Wrap(onRespErr, "OnResponse handler failed")
return
}
// If all retries failed, then wrap and return.
if doErr != nil {
doErr = RequestError{doErr}
return
}
// Response received, but with a failure status code:
if status < 200 || status > 299 {
// Try and parse the body.
var body = r.GetBody()
defer body.Close()
// This rarely happens, so we can (probably) make an exception for it.
buf := bytes.Buffer{}
buf.ReadFrom(body)
httpErr := &HTTPError{
Status: status,
Body: buf.Bytes(),
}
// Optionally unmarshal the error.
json.Unmarshal(httpErr.Body, httpErr)
doErr = httpErr
}
return
}