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