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 (
|
|
|
|
"context"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
2020-01-19 03:12:08 +00:00
|
|
|
"mime/multipart"
|
2020-01-02 05:39:52 +00:00
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2020-04-09 02:28:40 +00:00
|
|
|
"github.com/diamondburned/arikawa/utils/json"
|
2020-01-02 05:39:52 +00:00
|
|
|
)
|
|
|
|
|
2020-01-20 19:28:55 +00:00
|
|
|
// Retries is the default attempts to retry if the API returns an error before
|
|
|
|
// giving up.
|
|
|
|
var Retries uint = 5
|
|
|
|
|
2020-01-02 05:39:52 +00:00
|
|
|
type Client struct {
|
|
|
|
http.Client
|
|
|
|
json.Driver
|
2020-01-06 03:48:39 +00:00
|
|
|
SchemaEncoder
|
2020-01-20 19:28:55 +00:00
|
|
|
|
|
|
|
Retries uint
|
2020-01-02 05:39:52 +00:00
|
|
|
}
|
|
|
|
|
2020-01-15 04:43:34 +00:00
|
|
|
var DefaultClient = NewClient()
|
|
|
|
|
2020-01-02 05:39:52 +00:00
|
|
|
func NewClient() Client {
|
|
|
|
return Client{
|
|
|
|
Client: http.Client{
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
},
|
2020-01-06 03:48:39 +00:00
|
|
|
Driver: json.Default{},
|
|
|
|
SchemaEncoder: &DefaultSchema{},
|
2020-01-20 19:28:55 +00:00
|
|
|
Retries: Retries,
|
2020-01-02 05:39:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-19 03:12:08 +00:00
|
|
|
func (c *Client) MeanwhileMultipart(
|
|
|
|
multipartWriter func(*multipart.Writer) error,
|
2020-01-02 05:39:52 +00:00
|
|
|
method, url string, opts ...RequestOption) (*http.Response, error) {
|
|
|
|
|
|
|
|
// We want to cancel the request if our bodyWriter fails
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
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-01-19 03:12:08 +00:00
|
|
|
if err := multipartWriter(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
|
|
|
}()
|
|
|
|
|
|
|
|
resp, err := c.RequestCtx(ctx, method, url,
|
2020-01-19 03:12:08 +00:00
|
|
|
append([]RequestOption{
|
|
|
|
WithBody(r),
|
|
|
|
WithContentType(body.FormDataContentType()),
|
|
|
|
}, opts...)...)
|
2020-01-02 05:39:52 +00:00
|
|
|
|
|
|
|
if err != nil && bgErr != nil {
|
|
|
|
if resp.Body != nil {
|
|
|
|
resp.Body.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, bgErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) FastRequest(
|
|
|
|
method, url string, opts ...RequestOption) error {
|
2020-01-02 19:53:08 +00:00
|
|
|
|
2020-01-02 05:39:52 +00:00
|
|
|
r, err := c.Request(method, url, opts...)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return r.Body.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) RequestCtx(ctx context.Context,
|
|
|
|
method, url string, opts ...RequestOption) (*http.Response, error) {
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, RequestError{err}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, opt := range opts {
|
|
|
|
if err := opt(req); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-20 19:28:55 +00:00
|
|
|
var r *http.Response
|
|
|
|
|
|
|
|
for i := uint(0); i < c.Retries; i++ {
|
|
|
|
r, err = c.Client.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.StatusCode < 200 || r.StatusCode > 299 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
// If all retries failed:
|
2020-01-02 05:39:52 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, RequestError{err}
|
|
|
|
}
|
|
|
|
|
2020-01-20 19:28:55 +00:00
|
|
|
// Response received, but with a failure status code:
|
2020-01-02 05:39:52 +00:00
|
|
|
if r.StatusCode < 200 || r.StatusCode > 299 {
|
|
|
|
httpErr := &HTTPError{
|
|
|
|
Status: r.StatusCode,
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, httpErr
|
|
|
|
}
|
|
|
|
|
|
|
|
httpErr.Body = b
|
|
|
|
|
|
|
|
c.Unmarshal(b, &httpErr)
|
|
|
|
return nil, httpErr
|
|
|
|
}
|
|
|
|
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) RequestCtxJSON(ctx context.Context,
|
|
|
|
to interface{}, method, url string, opts ...RequestOption) error {
|
|
|
|
|
|
|
|
r, err := c.RequestCtx(ctx, method, url,
|
|
|
|
append([]RequestOption{JSONRequest}, opts...)...)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer r.Body.Close()
|
|
|
|
|
2020-01-19 02:27:30 +00:00
|
|
|
// No content, working as intended (tm)
|
|
|
|
if r.StatusCode == http.StatusNoContent {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-01-02 05:39:52 +00:00
|
|
|
if err := c.DecodeStream(r.Body, to); err != nil {
|
|
|
|
return JSONError{err}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) Request(
|
|
|
|
method, url string, opts ...RequestOption) (*http.Response, error) {
|
|
|
|
|
|
|
|
return c.RequestCtx(context.Background(), method, url, opts...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) RequestJSON(
|
|
|
|
to interface{}, method, url string, opts ...RequestOption) error {
|
|
|
|
|
|
|
|
return c.RequestCtxJSON(context.Background(), to, method, url, opts...)
|
|
|
|
}
|