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