RequestHandler.js
5.6 KB
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
'use strict';
const AsyncQueue = require('./AsyncQueue');
const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError');
const {
Events: { RATE_LIMIT },
browser,
} = require('../util/Constants');
const Util = require('../util/Util');
function parseResponse(res) {
if (res.headers.get('content-type').startsWith('application/json')) return res.json();
if (browser) return res.blob();
return res.buffer();
}
function getAPIOffset(serverDate) {
return new Date(serverDate).getTime() - Date.now();
}
function calculateReset(reset, serverDate) {
return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate);
}
class RequestHandler {
constructor(manager) {
this.manager = manager;
this.queue = new AsyncQueue();
this.reset = -1;
this.remaining = -1;
this.limit = -1;
this.retryAfter = -1;
}
async push(request) {
await this.queue.wait();
try {
return await this.execute(request);
} finally {
this.queue.shift();
}
}
get limited() {
return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset);
}
get _inactive() {
return this.queue.remaining === 0 && !this.limited;
}
async execute(request) {
// After calculations and requests have been done, pre-emptively stop further requests
if (this.limited) {
const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
if (this.manager.client.listenerCount(RATE_LIMIT)) {
/**
* Emitted when the client hits a rate limit while making a request
* @event Client#rateLimit
* @param {Object} rateLimitInfo Object containing the rate limit info
* @param {number} rateLimitInfo.timeout Timeout in ms
* @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint
* @param {string} rateLimitInfo.method HTTP method used for request that triggered this event
* @param {string} rateLimitInfo.path Path used for request that triggered this event
* @param {string} rateLimitInfo.route Route used for request that triggered this event
*/
this.manager.client.emit(RATE_LIMIT, {
timeout,
limit: this.limit,
method: request.method,
path: request.path,
route: request.route,
});
}
if (this.manager.globalTimeout) {
await this.manager.globalTimeout;
} else {
// Wait for the timeout to expire in order to avoid an actual 429
await Util.delayFor(timeout);
}
}
// Perform the request
let res;
try {
res = await request.make();
} catch (error) {
// Retry the specified number of times for request abortions
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path);
}
request.retries++;
return this.execute(request);
}
if (res && res.headers) {
const serverDate = res.headers.get('date');
const limit = res.headers.get('x-ratelimit-limit');
const remaining = res.headers.get('x-ratelimit-remaining');
const reset = res.headers.get('x-ratelimit-reset');
const retryAfter = res.headers.get('retry-after');
this.limit = limit ? Number(limit) : Infinity;
this.remaining = remaining ? Number(remaining) : 1;
this.reset = reset ? calculateReset(reset, serverDate) : Date.now();
this.retryAfter = retryAfter ? Number(retryAfter) : -1;
// https://github.com/discordapp/discord-api-docs/issues/182
if (request.route.includes('reactions')) {
this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250;
}
// Handle global ratelimit
if (res.headers.get('x-ratelimit-global')) {
// Set the manager's global timeout as the promise for other requests to "wait"
this.manager.globalTimeout = Util.delayFor(this.retryAfter);
// Wait for the global timeout to resolve before continuing
await this.manager.globalTimeout;
// Clean up global timeout
this.manager.globalTimeout = null;
}
}
// Handle 2xx and 3xx responses
if (res.ok) {
// Nothing wrong with the request, proceed with the next one
return parseResponse(res);
}
// Handle 4xx responses
if (res.status >= 400 && res.status < 500) {
// Handle ratelimited requests
if (res.status === 429) {
// A ratelimit was hit - this should never happen
this.manager.client.emit('debug', `429 hit on route ${request.route}`);
await Util.delayFor(this.retryAfter);
return this.execute(request);
}
// Handle possible malformed requests
let data;
try {
data = await parseResponse(res);
} catch (err) {
throw new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path);
}
throw new DiscordAPIError(request.path, data, request.method, res.status);
}
// Handle 5xx responses
if (res.status >= 500 && res.status < 600) {
// Retry the specified number of times for possible serverside issues
if (request.retries === this.manager.client.options.retryLimit) {
throw new HTTPError(res.statusText, res.constructor.name, res.status, request.method, request.path);
}
request.retries++;
return this.execute(request);
}
// Fallback in the rare case a status code outside the range 200..=599 is returned
return null;
}
}
module.exports = RequestHandler;