@ovencord/rest 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,472 @@
1
+ import { Collection } from '@ovencord/collection';
2
+ import { DiscordSnowflake } from '@sapphire/snowflake';
3
+ import { AsyncEventEmitter } from './utils/AsyncEventEmitter.js';
4
+ import { uuidv5 as uuidV5 } from './utils/utils.js';
5
+ import { CDN } from './CDN.js';
6
+ import { BurstHandler } from './handlers/BurstHandler.js';
7
+ import { SequentialHandler } from './handlers/SequentialHandler.js';
8
+ import type { IHandler } from './interfaces/Handler.js';
9
+ import {
10
+ AUTH_UUID_NAMESPACE,
11
+ BurstHandlerMajorIdKey,
12
+ DefaultRestOptions,
13
+ DefaultUserAgent,
14
+
15
+ RESTEvents,
16
+ } from './utils/constants.js';
17
+ import { RequestMethod } from './utils/types.js';
18
+ import type {
19
+ RESTOptions,
20
+ ResponseLike,
21
+ RestEvents,
22
+ HashData,
23
+ InternalRequest,
24
+ RouteLike,
25
+ RequestHeaders,
26
+ RouteData,
27
+ RequestData,
28
+ AuthData,
29
+ } from './utils/types.js';
30
+ import { isBufferLike, parseResponse } from './utils/utils.js';
31
+
32
+ /**
33
+ * Represents the class that manages handlers for endpoints
34
+ */
35
+ export class REST extends AsyncEventEmitter<RestEvents> {
36
+ /**
37
+ * The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} for all requests
38
+ * performed by this manager.
39
+ *
40
+ * @deprecated This property is deprecated and has no effect when using Bun native fetch.
41
+ * It is kept for API compatibility but will always be `null`.
42
+ */
43
+ public agent: null = null;
44
+
45
+ public readonly cdn: CDN;
46
+
47
+ /**
48
+ * The number of requests remaining in the global bucket
49
+ */
50
+ public globalRemaining: number;
51
+
52
+ /**
53
+ * The promise used to wait out the global rate limit
54
+ */
55
+ public globalDelay: Promise<void> | null = null;
56
+
57
+ /**
58
+ * The timestamp at which the global bucket resets
59
+ */
60
+ public globalReset = -1;
61
+
62
+ /**
63
+ * API bucket hashes that are cached from provided routes
64
+ */
65
+ public readonly hashes = new Collection<string, HashData>();
66
+
67
+ /**
68
+ * Request handlers created from the bucket hash and the major parameters
69
+ */
70
+ public readonly handlers = new Collection<string, IHandler>();
71
+
72
+ #token: string | null = null;
73
+
74
+ private hashTimer!: ReturnType<typeof setTimeout> | number;
75
+
76
+ private handlerTimer!: ReturnType<typeof setTimeout> | number;
77
+
78
+ public readonly options: RESTOptions;
79
+
80
+ public constructor(options: Partial<RESTOptions> = {}) {
81
+ super();
82
+ this.cdn = new CDN(options);
83
+ this.options = { ...DefaultRestOptions, ...options };
84
+ this.globalRemaining = Math.max(1, this.options.globalRequestsPerSecond);
85
+ this.agent = options.agent ?? null;
86
+
87
+ // Start sweepers
88
+ this.setupSweepers();
89
+ }
90
+
91
+ private setupSweepers() {
92
+ const validateMaxInterval = (interval: number) => {
93
+ if (interval > 14_400_000) {
94
+ throw new Error('Cannot set an interval greater than 4 hours');
95
+ }
96
+ };
97
+
98
+ if (this.options.hashSweepInterval !== 0 && this.options.hashSweepInterval !== Number.POSITIVE_INFINITY) {
99
+ validateMaxInterval(this.options.hashSweepInterval);
100
+ this.hashTimer = setInterval(() => {
101
+ const sweptHashes = new Collection<string, HashData>();
102
+ const currentDate = Date.now();
103
+
104
+ // Begin sweeping hash based on lifetimes
105
+ this.hashes.sweep((val, key) => {
106
+ // `-1` indicates a global hash
107
+ if (val.lastAccess === -1) return false;
108
+
109
+ // Check if lifetime has been exceeded
110
+ const shouldSweep = Math.floor(currentDate - val.lastAccess) > this.options.hashLifetime;
111
+
112
+ // Add hash to collection of swept hashes
113
+ if (shouldSweep) {
114
+ // Add to swept hashes
115
+ sweptHashes.set(key, val);
116
+
117
+ // Emit debug information
118
+ this.emit(RESTEvents.Debug, `[REST] Hash ${val.value} for ${key} swept due to lifetime being exceeded`);
119
+ }
120
+
121
+ return shouldSweep;
122
+ });
123
+
124
+ // Fire event
125
+ this.emit(RESTEvents.HashSweep, sweptHashes);
126
+ }, this.options.hashSweepInterval);
127
+
128
+ this.hashTimer.unref?.();
129
+ }
130
+
131
+ if (this.options.handlerSweepInterval !== 0 && this.options.handlerSweepInterval !== Number.POSITIVE_INFINITY) {
132
+ validateMaxInterval(this.options.handlerSweepInterval);
133
+ this.handlerTimer = setInterval(() => {
134
+ const sweptHandlers = new Collection<string, IHandler>();
135
+
136
+ // Begin sweeping handlers based on activity
137
+ this.handlers.sweep((val, key) => {
138
+ const { inactive } = val;
139
+
140
+ // Collect inactive handlers
141
+ if (inactive) {
142
+ sweptHandlers.set(key, val);
143
+ this.emit(RESTEvents.Debug, `[REST] Handler ${val.id} for ${key} swept due to being inactive`);
144
+ }
145
+
146
+ return inactive;
147
+ });
148
+
149
+ // Fire event
150
+ this.emit(RESTEvents.HandlerSweep, sweptHandlers);
151
+ }, this.options.handlerSweepInterval);
152
+
153
+ this.handlerTimer.unref?.();
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Runs a get request from the api
159
+ *
160
+ * @param fullRoute - The full route to query
161
+ * @param options - Optional request options
162
+ */
163
+ public async get(fullRoute: RouteLike, options: RequestData = {}) {
164
+ return this.request({ ...options, fullRoute, method: RequestMethod.Get });
165
+ }
166
+
167
+ /**
168
+ * Runs a delete request from the api
169
+ *
170
+ * @param fullRoute - The full route to query
171
+ * @param options - Optional request options
172
+ */
173
+ public async delete(fullRoute: RouteLike, options: RequestData = {}) {
174
+ return this.request({ ...options, fullRoute, method: RequestMethod.Delete });
175
+ }
176
+
177
+ /**
178
+ * Runs a post request from the api
179
+ *
180
+ * @param fullRoute - The full route to query
181
+ * @param options - Optional request options
182
+ */
183
+ public async post(fullRoute: RouteLike, options: RequestData = {}) {
184
+ return this.request({ ...options, fullRoute, method: RequestMethod.Post });
185
+ }
186
+
187
+ /**
188
+ * Runs a put request from the api
189
+ *
190
+ * @param fullRoute - The full route to query
191
+ * @param options - Optional request options
192
+ */
193
+ public async put(fullRoute: RouteLike, options: RequestData = {}) {
194
+ return this.request({ ...options, fullRoute, method: RequestMethod.Put });
195
+ }
196
+
197
+ /**
198
+ * Runs a patch request from the api
199
+ *
200
+ * @param fullRoute - The full route to query
201
+ * @param options - Optional request options
202
+ */
203
+ public async patch(fullRoute: RouteLike, options: RequestData = {}) {
204
+ return this.request({ ...options, fullRoute, method: RequestMethod.Patch });
205
+ }
206
+
207
+ /**
208
+ * Runs a request from the api
209
+ *
210
+ * @param options - Request options
211
+ */
212
+ public async request(options: InternalRequest) {
213
+ const response = await this.queueRequest(options);
214
+ return parseResponse(response);
215
+ }
216
+
217
+ /**
218
+ * Sets the default agent to use for requests performed by this manager
219
+ *
220
+ * @param agent - The agent to use
221
+ * @deprecated This method has no effect when using Bun native fetch. It is kept for API compatibility.
222
+ */
223
+ public setAgent(_agent: unknown) {
224
+ // No-op: Bun native fetch does not use agents
225
+ return this;
226
+ }
227
+
228
+ /**
229
+ * Sets the authorization token that should be used for requests
230
+ *
231
+ * @param token - The authorization token to use
232
+ */
233
+ public setToken(token: string) {
234
+ this.#token = token;
235
+ return this;
236
+ }
237
+
238
+ /**
239
+ * Queues a request to be sent
240
+ *
241
+ * @param request - All the information needed to make a request
242
+ * @returns The response from the api request
243
+ */
244
+ public async queueRequest(request: InternalRequest): Promise<ResponseLike> {
245
+ // Generalize the endpoint to its route data
246
+ const routeId = REST.generateRouteData(request.fullRoute, request.method);
247
+ const customAuth = typeof request.auth === 'object' && request.auth.token !== this.#token;
248
+ const auth = customAuth ? uuidV5((request.auth as AuthData).token, AUTH_UUID_NAMESPACE) : request.auth !== false;
249
+ // Get the bucket hash for the generic route, or point to a global route otherwise
250
+ const hash = this.hashes.get(`${request.method}:${routeId.bucketRoute}${customAuth ? `:${auth}` : ''}`) ?? {
251
+ value: `Global(${request.method}:${routeId.bucketRoute}${customAuth ? `:${auth}` : ''})`,
252
+ lastAccess: -1,
253
+ };
254
+
255
+ // Get the request handler for the obtained hash, with its major parameter
256
+ const handler =
257
+ this.handlers.get(`${hash.value}:${routeId.majorParameter}`) ??
258
+ this.createHandler(hash.value, routeId.majorParameter);
259
+
260
+ // Resolve the request into usable fetch options
261
+ const { url, fetchOptions } = await this.resolveRequest(request);
262
+
263
+ // Queue the request
264
+ return handler.queueRequest(routeId, url, fetchOptions, {
265
+ body: request.body,
266
+ files: request.files,
267
+ auth,
268
+ signal: request.signal,
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Creates a new rate limit handler from a hash, based on the hash and the major parameter
274
+ *
275
+ * @param hash - The hash for the route
276
+ * @param majorParameter - The major parameter for this handler
277
+ * @internal
278
+ */
279
+ private createHandler(hash: string, majorParameter: string) {
280
+ // Create the async request queue to handle requests
281
+ const queue =
282
+ majorParameter === BurstHandlerMajorIdKey
283
+ ? new BurstHandler(this, hash, majorParameter)
284
+ : new SequentialHandler(this, hash, majorParameter);
285
+ // Save the queue based on its id
286
+ this.handlers.set(queue.id, queue);
287
+
288
+ return queue;
289
+ }
290
+
291
+ /**
292
+ * Formats the request data to a usable format for fetch
293
+ *
294
+ * @param request - The request data
295
+ */
296
+ private async resolveRequest(request: InternalRequest): Promise<{ fetchOptions: RequestInit; url: string }> {
297
+ const { options } = this;
298
+
299
+ let query = '';
300
+
301
+ // If a query option is passed, use it
302
+ if (request.query) {
303
+ const resolvedQuery = request.query.toString();
304
+ if (resolvedQuery !== '') {
305
+ query = `?${resolvedQuery}`;
306
+ }
307
+ }
308
+
309
+ // Create the required headers
310
+ const headers: RequestHeaders = {
311
+ ...this.options.headers,
312
+ 'User-Agent': `${DefaultUserAgent} ${options.userAgentAppendix}`.trim(),
313
+ };
314
+
315
+ // If this request requires authorization (allowing non-"authorized" requests for webhooks)
316
+ if (request.auth !== false) {
317
+ if (typeof request.auth === 'object') {
318
+ headers.Authorization = `${request.auth.prefix ?? this.options.authPrefix} ${request.auth.token}`;
319
+ } else {
320
+ // If we haven't received a token, throw an error
321
+ if (!this.#token) {
322
+ throw new Error('Expected token to be set for this request, but none was present');
323
+ }
324
+
325
+ headers.Authorization = `${this.options.authPrefix} ${this.#token}`;
326
+ }
327
+ }
328
+
329
+ // If a reason was set, set its appropriate header
330
+ if (request.reason?.length) {
331
+ headers['X-Audit-Log-Reason'] = encodeURIComponent(request.reason);
332
+ }
333
+
334
+ // Format the full request URL (api base, optional version, endpoint, optional querystring)
335
+ const url = `${options.api}${request.versioned === false ? '' : `/v${options.version}`}${
336
+ request.fullRoute
337
+ }${query}`;
338
+
339
+ let finalBody: RequestInit['body'];
340
+ let additionalHeaders: Record<string, string> = {};
341
+
342
+ if (request.files?.length) {
343
+ const formData = new FormData();
344
+
345
+ // Attach all files to the request
346
+ for (const [index, file] of request.files.entries()) {
347
+ const fileKey = file.key ?? `files[${index}]`;
348
+
349
+ // https://developer.mozilla.org/docs/Web/API/FormData/append#parameters
350
+ // FormData.append only accepts a string or Blob.
351
+ // https://developer.mozilla.org/docs/Web/API/Blob/Blob#parameters
352
+ // The Blob constructor accepts TypedArray/ArrayBuffer, strings, and Blobs.
353
+ if (isBufferLike(file.data)) {
354
+ // Use provided content type or default to application/octet-stream
355
+ const contentType = file.contentType ?? 'application/octet-stream';
356
+
357
+ formData.append(fileKey, new Blob([file.data as any], { type: contentType }), file.name);
358
+ } else {
359
+ formData.append(
360
+ fileKey,
361
+ new Blob([`${file.data}`], { type: file.contentType } as BlobPropertyBag),
362
+ file.name,
363
+ );
364
+ }
365
+ }
366
+
367
+ // If a JSON body was added as well, attach it to the form data, using payload_json unless otherwise specified
368
+ // eslint-disable-next-line no-eq-null, eqeqeq
369
+ if (request.body != null) {
370
+ if (request.appendToFormData) {
371
+ for (const [key, value] of Object.entries(request.body as Record<string, unknown>)) {
372
+ formData.append(key, value as string | Blob);
373
+ }
374
+ } else {
375
+ formData.append('payload_json', JSON.stringify(request.body));
376
+ }
377
+ }
378
+
379
+ // Set the final body to the form data
380
+ finalBody = formData;
381
+
382
+ // eslint-disable-next-line no-eq-null, eqeqeq
383
+ } else if (request.body != null) {
384
+ if (request.passThroughBody) {
385
+ finalBody = request.body as BodyInit;
386
+ } else {
387
+ // Stringify the JSON data
388
+ finalBody = JSON.stringify(request.body);
389
+ // Set the additional headers to specify the content-type
390
+ additionalHeaders = { 'Content-Type': 'application/json' };
391
+ }
392
+ }
393
+
394
+ const method = request.method.toUpperCase();
395
+
396
+ // The non null assertions in the following block are due to exactOptionalPropertyTypes, they have been tested to work with undefined
397
+ const fetchOptions: RequestInit = {
398
+ // Set body to null on get / head requests. This does not follow fetch spec (likely because it causes subtle bugs) but is aligned with what request was doing
399
+ body: ['GET', 'HEAD'].includes(method) ? null : finalBody!,
400
+ headers: { ...request.headers, ...additionalHeaders, ...headers } as Record<string, string>,
401
+ method,
402
+ // Prioritize setting an agent per request, use the agent for this instance otherwise.
403
+ // dispatcher is deprecated and ignored in Bun native fetch
404
+ };
405
+
406
+ return { url, fetchOptions };
407
+ }
408
+
409
+ /**
410
+ * Stops the hash sweeping interval
411
+ */
412
+ public clearHashSweeper() {
413
+ clearInterval(this.hashTimer);
414
+ }
415
+
416
+ /**
417
+ * Stops the request handler sweeping interval
418
+ */
419
+ public clearHandlerSweeper() {
420
+ clearInterval(this.handlerTimer);
421
+ }
422
+
423
+ /**
424
+ * Generates route data for an endpoint:method
425
+ *
426
+ * @param endpoint - The raw endpoint to generalize
427
+ * @param method - The HTTP method this endpoint is called without
428
+ * @internal
429
+ */
430
+ private static generateRouteData(endpoint: RouteLike, method: RequestMethod): RouteData {
431
+ if (endpoint.startsWith('/interactions/') && endpoint.endsWith('/callback')) {
432
+ return {
433
+ majorParameter: BurstHandlerMajorIdKey,
434
+ bucketRoute: '/interactions/:id/:token/callback',
435
+ original: endpoint,
436
+ };
437
+ }
438
+
439
+ const majorIdMatch = /(?:^\/webhooks\/(\d{17,19}\/[^/?]+))|(?:^\/(?:channels|guilds|webhooks)\/(\d{17,19}))/.exec(
440
+ endpoint,
441
+ );
442
+
443
+ // Get the major id or id + token for this route - global otherwise
444
+ const majorId = majorIdMatch?.[2] ?? majorIdMatch?.[1] ?? 'global';
445
+
446
+ const baseRoute = endpoint
447
+ // Strip out all ids
448
+ .replaceAll(/\d{17,19}/g, ':id')
449
+ // Strip out reaction as they fall under the same bucket
450
+ .replace(/\/reactions\/(.*)/, '/reactions/:reaction')
451
+ // Strip out webhook tokens
452
+ .replace(/\/webhooks\/:id\/[^/?]+/, '/webhooks/:id/:token');
453
+
454
+ let exceptions = '';
455
+
456
+ // Hard-Code Old Message Deletion Exception (2 week+ old messages are a different bucket)
457
+ // https://github.com/discord/discord-api-docs/issues/1295
458
+ if (method === RequestMethod.Delete && baseRoute === '/channels/:id/messages/:id') {
459
+ const id = /\d{17,19}$/.exec(endpoint)![0]!;
460
+ const timestamp = DiscordSnowflake.timestampFrom(id);
461
+ if (Date.now() - timestamp > 1_000 * 60 * 60 * 24 * 14) {
462
+ exceptions += '/Delete Old Message';
463
+ }
464
+ }
465
+
466
+ return {
467
+ majorParameter: majorId,
468
+ bucketRoute: baseRoute + exceptions,
469
+ original: endpoint,
470
+ };
471
+ }
472
+ }
@@ -0,0 +1,116 @@
1
+ import type { InternalRequest, RawFile } from '../utils/types.js';
2
+
3
+ export interface DiscordErrorFieldInformation {
4
+ code: string;
5
+ message: string;
6
+ }
7
+
8
+ export interface DiscordErrorGroupWrapper {
9
+ _errors: DiscordError[];
10
+ }
11
+
12
+ export type DiscordError =
13
+ | DiscordErrorFieldInformation
14
+ | DiscordErrorGroupWrapper
15
+ | string
16
+ | { [k: string]: DiscordError };
17
+
18
+ export interface DiscordErrorData {
19
+ code: number;
20
+ errors?: DiscordError;
21
+ message: string;
22
+ }
23
+
24
+ export interface OAuthErrorData {
25
+ error: string;
26
+ error_description?: string;
27
+ }
28
+
29
+ export interface RequestBody {
30
+ files: RawFile[] | undefined;
31
+ json: unknown | undefined;
32
+ }
33
+
34
+ function isErrorGroupWrapper(error: DiscordError): error is DiscordErrorGroupWrapper {
35
+ return Reflect.has(error as Record<string, unknown>, '_errors');
36
+ }
37
+
38
+ function isErrorResponse(error: DiscordError): error is DiscordErrorFieldInformation {
39
+ return typeof Reflect.get(error as Record<string, unknown>, 'message') === 'string';
40
+ }
41
+
42
+ /**
43
+ * Represents an API error returned by Discord
44
+ */
45
+ export class DiscordAPIError extends Error {
46
+ public requestBody: RequestBody;
47
+
48
+ /**
49
+ * @param rawError - The error reported by Discord
50
+ * @param code - The error code reported by Discord
51
+ * @param status - The status code of the response
52
+ * @param method - The method of the request that errored
53
+ * @param url - The url of the request that errored
54
+ * @param bodyData - The unparsed data for the request that errored
55
+ */
56
+ public constructor(
57
+ public rawError: DiscordErrorData | OAuthErrorData,
58
+ public code: number | string,
59
+ public status: number,
60
+ public method: string,
61
+ public url: string,
62
+ bodyData: Pick<InternalRequest, 'body' | 'files'>,
63
+ ) {
64
+ super(DiscordAPIError.getMessage(rawError));
65
+
66
+ this.requestBody = { files: bodyData.files, json: bodyData.body };
67
+ }
68
+
69
+ /**
70
+ * The name of the error
71
+ */
72
+ public override get name(): string {
73
+ return `${DiscordAPIError.name}[${this.code}]`;
74
+ }
75
+
76
+ private static getMessage(error: DiscordErrorData | OAuthErrorData) {
77
+ let flattened = '';
78
+ if ('code' in error) {
79
+ if (error.errors) {
80
+ flattened = [...this.flattenDiscordError(error.errors)].join('\n');
81
+ }
82
+
83
+ return error.message && flattened
84
+ ? `${error.message}\n${flattened}`
85
+ : error.message || flattened || 'Unknown Error';
86
+ }
87
+
88
+ return error.error_description ?? 'No Description';
89
+ }
90
+
91
+ private static *flattenDiscordError(obj: DiscordError, key = ''): IterableIterator<string> {
92
+ if (isErrorResponse(obj)) {
93
+ return yield `${key.length ? `${key}[${obj.code}]` : `${obj.code}`}: ${obj.message}`.trim();
94
+ }
95
+
96
+ for (const [otherKey, val] of Object.entries(obj)) {
97
+ const nextKey = otherKey.startsWith('_')
98
+ ? key
99
+ : key
100
+ ? Number.isNaN(Number(otherKey))
101
+ ? `${key}.${otherKey}`
102
+ : `${key}[${otherKey}]`
103
+ : otherKey;
104
+
105
+ if (typeof val === 'string') {
106
+ yield val;
107
+ } else if (isErrorGroupWrapper(val)) {
108
+ for (const error of val._errors) {
109
+ yield* this.flattenDiscordError(error, nextKey);
110
+ }
111
+ } else {
112
+ yield* this.flattenDiscordError(val, nextKey);
113
+ }
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,29 @@
1
+ import type { InternalRequest } from '../utils/types.js';
2
+ import type { RequestBody } from './DiscordAPIError.js';
3
+
4
+ /**
5
+ * Represents a HTTP error
6
+ */
7
+ export class HTTPError extends Error {
8
+ public requestBody: RequestBody;
9
+
10
+ public override name = HTTPError.name;
11
+
12
+ /**
13
+ * @param status - The status code of the response
14
+ * @param statusText - The status text of the response
15
+ * @param method - The method of the request that errored
16
+ * @param url - The url of the request that errored
17
+ * @param bodyData - The unparsed data for the request that errored
18
+ */
19
+ public constructor(
20
+ public status: number,
21
+ statusText: string,
22
+ public method: string,
23
+ public url: string,
24
+ bodyData: Pick<InternalRequest, 'body' | 'files'>,
25
+ ) {
26
+ super(statusText);
27
+ this.requestBody = { files: bodyData.files, json: bodyData.body };
28
+ }
29
+ }
@@ -0,0 +1,47 @@
1
+ import type { RateLimitData } from '../utils/types.js';
2
+
3
+ export class RateLimitError extends Error implements RateLimitData {
4
+ public timeToReset: number;
5
+
6
+ public limit: number;
7
+
8
+ public method: string;
9
+
10
+ public hash: string;
11
+
12
+ public url: string;
13
+
14
+ public route: string;
15
+
16
+ public majorParameter: string;
17
+
18
+ public global: boolean;
19
+
20
+ public retryAfter: number;
21
+
22
+ public sublimitTimeout: number;
23
+
24
+ public scope: RateLimitData['scope'];
25
+
26
+ public constructor(data: RateLimitData) {
27
+ super();
28
+ this.timeToReset = data.timeToReset;
29
+ this.limit = data.limit;
30
+ this.method = data.method;
31
+ this.hash = data.hash;
32
+ this.url = data.url;
33
+ this.route = data.route;
34
+ this.majorParameter = data.majorParameter;
35
+ this.global = data.global;
36
+ this.retryAfter = data.retryAfter;
37
+ this.sublimitTimeout = data.sublimitTimeout;
38
+ this.scope = data.scope;
39
+ }
40
+
41
+ /**
42
+ * The name of the error
43
+ */
44
+ public override get name(): string {
45
+ return `${RateLimitError.name}[${this.route}]`;
46
+ }
47
+ }