RequestHandler.js (6002B)
1 'use strict'; 2 3 const DiscordAPIError = require('./DiscordAPIError'); 4 const HTTPError = require('./HTTPError'); 5 const { 6 Events: { RATE_LIMIT }, 7 browser, 8 } = require('../util/Constants'); 9 const Util = require('../util/Util'); 10 11 function parseResponse(res) { 12 if (res.headers.get('content-type').startsWith('application/json')) return res.json(); 13 if (browser) return res.blob(); 14 return res.buffer(); 15 } 16 17 function getAPIOffset(serverDate) { 18 return new Date(serverDate).getTime() - Date.now(); 19 } 20 21 function calculateReset(reset, serverDate) { 22 return new Date(Number(reset) * 1000).getTime() - getAPIOffset(serverDate); 23 } 24 25 class RequestHandler { 26 constructor(manager) { 27 this.manager = manager; 28 this.busy = false; 29 this.queue = []; 30 this.reset = -1; 31 this.remaining = -1; 32 this.limit = -1; 33 this.retryAfter = -1; 34 } 35 36 push(request) { 37 if (this.busy) { 38 this.queue.push(request); 39 return this.run(); 40 } else { 41 return this.execute(request); 42 } 43 } 44 45 run() { 46 if (this.queue.length === 0) return Promise.resolve(); 47 return this.execute(this.queue.shift()); 48 } 49 50 get limited() { 51 return Boolean(this.manager.globalTimeout) || (this.remaining <= 0 && Date.now() < this.reset); 52 } 53 54 get _inactive() { 55 return this.queue.length === 0 && !this.limited && this.busy !== true; 56 } 57 58 async execute(item) { 59 // Insert item back to the beginning if currently busy 60 if (this.busy) { 61 this.queue.unshift(item); 62 return null; 63 } 64 65 this.busy = true; 66 const { reject, request, resolve } = item; 67 68 // After calculations and requests have been done, pre-emptively stop further requests 69 if (this.limited) { 70 const timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now(); 71 72 if (this.manager.client.listenerCount(RATE_LIMIT)) { 73 /** 74 * Emitted when the client hits a rate limit while making a request 75 * @event Client#rateLimit 76 * @param {Object} rateLimitInfo Object containing the rate limit info 77 * @param {number} rateLimitInfo.timeout Timeout in ms 78 * @param {number} rateLimitInfo.limit Number of requests that can be made to this endpoint 79 * @param {string} rateLimitInfo.method HTTP method used for request that triggered this event 80 * @param {string} rateLimitInfo.path Path used for request that triggered this event 81 * @param {string} rateLimitInfo.route Route used for request that triggered this event 82 */ 83 this.manager.client.emit(RATE_LIMIT, { 84 timeout, 85 limit: this.limit, 86 method: request.method, 87 path: request.path, 88 route: request.route, 89 }); 90 } 91 92 if (this.manager.globalTimeout) { 93 await this.manager.globalTimeout; 94 } else { 95 // Wait for the timeout to expire in order to avoid an actual 429 96 await Util.delayFor(timeout); 97 } 98 } 99 100 // Perform the request 101 let res; 102 try { 103 res = await request.make(); 104 } catch (error) { 105 // NodeFetch error expected for all "operational" errors, such as 500 status code 106 this.busy = false; 107 return reject(new HTTPError(error.message, error.constructor.name, error.status, request.method, request.path)); 108 } 109 110 if (res && res.headers) { 111 const serverDate = res.headers.get('date'); 112 const limit = res.headers.get('x-ratelimit-limit'); 113 const remaining = res.headers.get('x-ratelimit-remaining'); 114 const reset = res.headers.get('x-ratelimit-reset'); 115 const retryAfter = res.headers.get('retry-after'); 116 117 this.limit = limit ? Number(limit) : Infinity; 118 this.remaining = remaining ? Number(remaining) : 1; 119 this.reset = reset ? calculateReset(reset, serverDate) : Date.now(); 120 this.retryAfter = retryAfter ? Number(retryAfter) : -1; 121 122 // https://github.com/discordapp/discord-api-docs/issues/182 123 if (item.request.route.includes('reactions')) { 124 this.reset = new Date(serverDate).getTime() - getAPIOffset(serverDate) + 250; 125 } 126 127 // Handle global ratelimit 128 if (res.headers.get('x-ratelimit-global')) { 129 // Set the manager's global timeout as the promise for other requests to "wait" 130 this.manager.globalTimeout = Util.delayFor(this.retryAfter); 131 132 // Wait for the global timeout to resolve before continuing 133 await this.manager.globalTimeout; 134 135 // Clean up global timeout 136 this.manager.globalTimeout = null; 137 } 138 } 139 140 // Finished handling headers, safe to unlock manager 141 this.busy = false; 142 143 if (res.ok) { 144 const success = await parseResponse(res); 145 // Nothing wrong with the request, proceed with the next one 146 resolve(success); 147 return this.run(); 148 } else if (res.status === 429) { 149 // A ratelimit was hit - this should never happen 150 this.queue.unshift(item); 151 this.manager.client.emit('debug', `429 hit on route ${item.request.route}`); 152 await Util.delayFor(this.retryAfter); 153 return this.run(); 154 } else if (res.status >= 500 && res.status < 600) { 155 // Retry the specified number of times for possible serverside issues 156 if (item.retries === this.manager.client.options.retryLimit) { 157 return reject( 158 new HTTPError(res.statusText, res.constructor.name, res.status, item.request.method, request.path), 159 ); 160 } else { 161 item.retries++; 162 this.queue.unshift(item); 163 return this.run(); 164 } 165 } else { 166 // Handle possible malformed requests 167 try { 168 const data = await parseResponse(res); 169 if (res.status >= 400 && res.status < 500) { 170 return reject(new DiscordAPIError(request.path, data, request.method, res.status)); 171 } 172 return null; 173 } catch (err) { 174 return reject(new HTTPError(err.message, err.constructor.name, err.status, request.method, request.path)); 175 } 176 } 177 } 178 } 179 180 module.exports = RequestHandler;