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