-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
client.go
377 lines (285 loc) · 9.21 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
package internal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/Kyagara/equinox/api"
"go.uber.org/zap"
)
type InternalClient struct {
key string
Cluster api.Cluster
http *http.Client
logLevel api.LogLevel
logger *zap.SugaredLogger
defaultTTL int64
cache *Cache
rateLimit bool
rates map[interface{}]*RateLimit
retry bool
}
// Creates an EquinoxConfig for tests.
func NewTestEquinoxConfig() *api.EquinoxConfig {
return &api.EquinoxConfig{
Key: "RGAPI-KEY",
Cluster: api.AmericasCluster,
LogLevel: api.DebugLevel,
Timeout: 10,
TTL: 0,
Retry: false,
RateLimit: false,
}
}
// Returns a new InternalClient using configuration object provided.
func NewInternalClient(config *api.EquinoxConfig) *InternalClient {
return &InternalClient{
key: config.Key,
Cluster: config.Cluster,
http: &http.Client{Timeout: time.Duration(config.Timeout * int(time.Second))},
logger: NewLogger(config),
logLevel: config.LogLevel,
defaultTTL: config.TTL * int64(time.Second),
cache: NewCache(),
rates: map[interface{}]*RateLimit{},
rateLimit: config.RateLimit,
retry: config.Retry,
}
}
func (c *InternalClient) ClearInternalClientCache() {
if c.defaultTTL > 0 {
c.cache.Clear()
}
}
// Performs a GET request, authorizationHeader can be blank
func (c *InternalClient) Get(route interface{}, endpoint string, object interface{}, endpointName string, method string, authorizationHeader string) error {
baseUrl := fmt.Sprintf(api.BaseURLFormat, route)
// Creating a new HTTP Request.
req, err := c.newRequest(http.MethodGet, fmt.Sprintf("%s%s", baseUrl, endpoint), nil)
if err != nil {
return err
}
if authorizationHeader != "" {
req.Header.Set("Authorization", authorizationHeader)
}
// If caching is enabled
if c.defaultTTL > 0 {
cacheItem, err := c.cache.Get(req.URL.String())
if err != nil {
return err
}
if cacheItem != nil {
logger := c.logger.With("httpMethod", http.MethodGet, "path", req.URL.Path)
logger.Info("Cache hit")
if err != nil {
logger.Error(err)
return err
}
// Decoding the cached body into the endpoint method response object.
err = json.Unmarshal(cacheItem.response, &object)
if err != nil {
return err
}
return nil
}
}
// Sending HTTP request and returning the response.
_, body, err := c.sendRequest(req, false, endpointName, method, route)
if err != nil {
return err
}
if c.defaultTTL > 0 {
c.cache.Set(req.URL.String(), body, c.defaultTTL)
}
// Decoding the body into the endpoint method response object.
err = json.Unmarshal(body, &object)
if err != nil {
return err
}
return nil
}
// Performs a POST request, authorizationHeader can be blank
func (c *InternalClient) Post(route interface{}, endpoint string, requestBody interface{}, object interface{}, endpointName string, method string, authorizationHeader string) error {
baseUrl := fmt.Sprintf(api.BaseURLFormat, route)
// Creating a new HTTP Request.
req, err := c.newRequest(http.MethodPost, fmt.Sprintf("%s%s", baseUrl, endpoint), requestBody)
if err != nil {
return err
}
if authorizationHeader != "" {
req.Header.Set("Authorization", authorizationHeader)
}
// Sending HTTP request and returning the response.
res, body, err := c.sendRequest(req, false, endpointName, method, route)
if err != nil {
return err
}
// In case of a post request returning just a single, non JSON response.
// This requires the endpoint method to handle the response as a api.PlainTextResponse and do type assertion.
// This implementation looks horrible, I don't know another way of decoding any non JSON value to the &object.
if res.Header.Get("Content-Type") == "" {
body := []byte(fmt.Sprintf(`{"response":"%s"}`, string(body)))
err = json.Unmarshal(body, &object)
if err != nil {
return err
}
return nil
}
// Decoding the body into the endpoint method response object.
err = json.Unmarshal(body, &object)
if err != nil {
return err
}
return nil
}
// Performs a PUT request
func (c *InternalClient) Put(route interface{}, endpoint string, requestBody interface{}, endpointName string, method string) error {
baseUrl := fmt.Sprintf(api.BaseURLFormat, route)
// Creating a new HTTP Request.
req, err := c.newRequest(http.MethodPut, fmt.Sprintf("%s%s", baseUrl, endpoint), requestBody)
if err != nil {
return err
}
// Sending HTTP request and returning the response.
_, _, err = c.sendRequest(req, false, endpointName, method, route)
if err != nil {
return err
}
return nil
}
// Sends a HTTP request.
func (c *InternalClient) sendRequest(req *http.Request, retried bool, endpoint string, method string, route interface{}) (*http.Response, []byte, error) {
logger := c.logger.With("httpMethod", req.Method, "path", req.URL.Path)
// If rate limiting is enabled
if c.rateLimit {
isRateLimited := c.checkRates(route, endpoint, method)
if isRateLimited {
return nil, nil, api.TooManyRequestsError
}
}
logger.Debug("Making request")
// Sending request.
res, err := c.http.Do(req)
if err != nil {
logger.Error("Request failed")
return nil, nil, err
}
defer res.Body.Close()
// Update rate limits
if c.rateLimit && res.Header.Get("X-App-Rate-Limit") != "" {
// Updating app rate limit
rate := ParseHeaders(res.Header, "X-App-Rate-Limit", "X-App-Rate-Limit-Count")
c.rates[route].SetAppRate(rate)
// Updating method rate limit
rate = ParseHeaders(res.Header, "X-Method-Rate-Limit", "X-Method-Rate-Limit-Count")
c.rates[route].Set(endpoint, method, rate)
}
// Checking the response
err = c.checkResponse(res)
// The body is defined here so if we retry the request we can later return the value
// without having to read the body again, causing an error
var body []byte
// If retry is enabled and c.checkResponse() returns an api.RateLimitedError, retry the request
if c.retry && errors.Is(err, api.TooManyRequestsError) && !retried {
// If this retry is successful, the body var will be the res.Body
res, body, err = c.sendRequest(req, true, endpoint, method, route)
}
// Returns the error from c.checkResponse() if any
// If retry is enabled, this error could also be the error from the retried request if it failed again
if err != nil {
return nil, nil, err
}
// If the retry was successful, the body won't be nil, so return the result here to avoid reading the body again
if body != nil {
return res, body, nil
}
logger.Info("Request successful")
body, err = ioutil.ReadAll(res.Body)
if err != nil {
logger.Error(err)
return nil, nil, err
}
return res, body, nil
}
// Creates a new HTTP Request and sets headers.
func (c *InternalClient) newRequest(method string, url string, body interface{}) (*http.Request, error) {
var buffer io.ReadWriter
if body != nil {
buffer = &bytes.Buffer{}
enc := json.NewEncoder(buffer)
err := enc.Encode(body)
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(method, url, buffer)
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Riot-Token", c.key)
req.Header.Set("User-Agent", "equinox")
return req, nil
}
func (c *InternalClient) checkResponse(res *http.Response) error {
logger := c.logger.With("httpMethod", res.Request.Method, "path", res.Request.URL.Path)
// If the API returns a 429 code.
if res.StatusCode == http.StatusTooManyRequests && c.retry {
retryAfter := res.Header.Get("Retry-After")
// If the header isn't found, don't retry and return error.
if retryAfter == "" {
return fmt.Errorf("rate limited but no Retry-After header was found, stopping")
}
seconds, _ := strconv.Atoi(retryAfter)
logger.Warn(fmt.Sprintf("Too Many Requests, retrying request in %ds", seconds))
time.Sleep(time.Duration(seconds) * time.Second)
return api.TooManyRequestsError
}
// If the status code is lower than 200 or higher than 299, return an error.
if res.StatusCode < http.StatusOK || res.StatusCode > 299 {
logger.Errorf("Endpoint method returned an error response: %v", res.Status)
// Handling errors documented in the Riot API docs
// This StatusCodeToError solution is from KnutZuidema/golio
// https://github.com/KnutZuidema/golio/blob/master/api/error.go
// https://github.com/KnutZuidema/golio/blob/master/internal/client.go
err, ok := api.StatusCodeToError[res.StatusCode]
if !ok {
err = api.ErrorResponse{
Status: api.Status{
Message: "Unknown error",
StatusCode: res.StatusCode,
},
}
}
return err
}
return nil
}
// Checks the app and method rate limit, returns true if rate limited
func (c *InternalClient) checkRates(route interface{}, endpoint string, method string) bool {
if c.rates[route] == nil {
c.rates[route] = NewRateLimit()
}
// Checking rate limits for the app
isRateLimited := c.rates[route].IsRateLimited(c.rates[route].appRate)
if isRateLimited {
return true
}
// Checking rate limits for the endpoint method
rate := c.rates[route].Get(endpoint, method)
if rate != nil {
isRateLimited := c.rates[route].IsRateLimited(rate)
if isRateLimited {
return true
}
}
return false
}