arikawa/utils/httputil/client.go

214 lines
4.6 KiB
Go
Raw Normal View History

2020-01-15 04:56:50 +00:00
// Package httputil provides abstractions around the common needs of HTTP. It
// also allows swapping in and out the HTTP client.
2020-01-02 05:39:52 +00:00
package httputil
import (
2020-04-19 21:53:53 +00:00
"bytes"
2020-01-02 05:39:52 +00:00
"context"
"io"
2020-01-19 03:12:08 +00:00
"mime/multipart"
2020-01-02 05:39:52 +00:00
2020-04-19 21:53:53 +00:00
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
"github.com/diamondburned/arikawa/utils/json"
2020-04-19 21:53:53 +00:00
"github.com/pkg/errors"
2020-01-02 05:39:52 +00:00
)
// Retries is the default attempts to retry if the API returns an error before
2020-04-19 21:53:53 +00:00
// giving up. If the value is smaller than 1, then requests will retry forever.
var Retries uint = 5
2020-01-02 05:39:52 +00:00
type Client struct {
2020-04-19 21:53:53 +00:00
httpdriver.Client
2020-01-02 05:39:52 +00:00
json.Driver
2020-01-06 03:48:39 +00:00
SchemaEncoder
2020-05-03 21:02:03 +00:00
// OnRequest, if not nil, will be copied and prefixed on each Request.
OnRequest []RequestOption
2020-04-19 21:53:53 +00:00
// 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.
2020-05-03 21:02:03 +00:00
OnResponse []ResponseFunc
2020-04-19 21:53:53 +00:00
// Default to the global Retries variable (5).
Retries uint
2020-01-02 05:39:52 +00:00
2020-05-03 21:02:03 +00:00
context context.Context
2020-04-19 21:53:53 +00:00
}
2020-01-15 04:43:34 +00:00
2020-04-19 21:53:53 +00:00
func NewClient() *Client {
return &Client{
Client: httpdriver.NewClient(),
Driver: json.Default,
2020-01-06 03:48:39 +00:00
SchemaEncoder: &DefaultSchema{},
Retries: Retries,
2020-05-03 21:02:03 +00:00
context: context.Background(),
2020-04-19 21:53:53 +00:00
}
}
2020-05-03 21:02:03 +00:00
// 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
}
2020-04-19 21:53:53 +00:00
func (c *Client) applyOptions(r httpdriver.Request, extra []RequestOption) error {
2020-05-03 21:02:03 +00:00
for _, opt := range c.OnRequest {
2020-04-19 21:53:53 +00:00
if err := opt(r); err != nil {
return err
}
2020-01-02 05:39:52 +00:00
}
2020-04-19 21:53:53 +00:00
for _, opt := range extra {
if err := opt(r); err != nil {
return err
}
}
return nil
2020-01-02 05:39:52 +00:00
}
2020-01-19 03:12:08 +00:00
func (c *Client) MeanwhileMultipart(
2020-04-19 21:53:53 +00:00
writer func(*multipart.Writer) error,
method, url string, opts ...RequestOption) (httpdriver.Response, error) {
2020-01-02 05:39:52 +00:00
2020-05-03 21:02:03 +00:00
// We want to cancel the request if our bodyWriter fails.
ctx, cancel := context.WithCancel(c.context)
2020-01-19 02:27:30 +00:00
defer cancel()
2020-01-02 05:39:52 +00:00
r, w := io.Pipe()
2020-01-19 03:12:08 +00:00
body := multipart.NewWriter(w)
2020-01-02 05:39:52 +00:00
var bgErr error
go func() {
2020-04-19 21:53:53 +00:00
if err := writer(body); err != nil {
2020-01-02 05:39:52 +00:00
bgErr = err
cancel()
}
2020-01-19 02:27:30 +00:00
// Close the writer so the body gets flushed to the HTTP reader.
w.Close()
2020-01-02 05:39:52 +00:00
}()
2020-04-19 21:53:53 +00:00
// Prepend the multipart writer and the correct Content-Type header options.
opts = PrependOptions(
opts,
WithBody(r),
WithContentType(body.FormDataContentType()),
)
2020-01-02 05:39:52 +00:00
2020-05-03 21:02:03 +00:00
// Request with the current client and our own context:
resp, err := c.WithContext(ctx).Request(method, url, opts...)
2020-01-02 05:39:52 +00:00
if err != nil && bgErr != nil {
return nil, bgErr
}
return resp, err
}
2020-04-19 21:53:53 +00:00
func (c *Client) FastRequest(method, url string, opts ...RequestOption) error {
2020-01-02 05:39:52 +00:00
r, err := c.Request(method, url, opts...)
if err != nil {
return err
}
2020-04-19 21:53:53 +00:00
return r.GetBody().Close()
}
2020-05-03 21:02:03 +00:00
func (c *Client) RequestJSON(to interface{}, method, url string, opts ...RequestOption) error {
2020-04-19 21:53:53 +00:00
opts = PrependOptions(opts, JSONRequest)
2020-05-03 21:02:03 +00:00
r, err := c.Request(method, url, opts...)
2020-04-19 21:53:53 +00:00
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
}
if err := c.DecodeStream(body, to); err != nil {
return JSONError{err}
}
return nil
2020-01-02 05:39:52 +00:00
}
2020-05-03 21:02:03 +00:00
func (c *Client) Request(method, url string, opts ...RequestOption) (httpdriver.Response, error) {
req, err := c.Client.NewRequest(c.context, method, url)
2020-01-02 05:39:52 +00:00
if err != nil {
return nil, RequestError{err}
}
2020-04-19 21:53:53 +00:00
if err := c.applyOptions(req, opts); err != nil {
return nil, errors.Wrap(err, "Failed to apply options")
2020-01-02 05:39:52 +00:00
}
2020-04-19 21:53:53 +00:00
var r httpdriver.Response
var status int
2020-04-19 21:53:53 +00:00
for i := uint(0); c.Retries < 1 || i < c.Retries; i++ {
r, err = c.Client.Do(req)
if err != nil {
continue
}
2020-04-19 21:53:53 +00:00
if status = r.GetStatus(); status < 200 || status > 299 {
continue
}
break
}
2020-04-19 21:53:53 +00:00
// Call OnResponse() even if the request failed.
2020-05-03 21:02:03 +00:00
for _, fn := range c.OnResponse {
if err := fn(req, r); err != nil {
return nil, err
}
2020-04-19 21:53:53 +00:00
}
// If all retries failed:
2020-01-02 05:39:52 +00:00
if err != nil {
return nil, RequestError{err}
}
// Response received, but with a failure status code:
2020-04-19 21:53:53 +00:00
if status < 200 || status > 299 {
// Try and parse the body.
var body = r.GetBody()
defer body.Close()
2020-01-02 05:39:52 +00:00
2020-04-19 21:53:53 +00:00
// 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(),
2020-01-02 05:39:52 +00:00
}
2020-04-19 21:53:53 +00:00
// Optionally unmarshal the error.
c.Unmarshal(httpErr.Body, &httpErr)
2020-01-02 05:39:52 +00:00
return nil, httpErr
}
return r, nil
}