-
-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Shared.ts
153 lines (140 loc) · 5.39 KB
/
Shared.ts
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
import type { RequestInit } from 'undici';
import type { REST } from '../REST.js';
import type { DiscordErrorData, OAuthErrorData } from '../errors/DiscordAPIError.js';
import { DiscordAPIError } from '../errors/DiscordAPIError.js';
import { HTTPError } from '../errors/HTTPError.js';
import { RESTEvents } from '../utils/constants.js';
import type { ResponseLike, HandlerRequestData, RouteData } from '../utils/types.js';
import { parseResponse, shouldRetry } from '../utils/utils.js';
/**
* Invalid request limiting is done on a per-IP basis, not a per-token basis.
* The best we can do is track invalid counts process-wide (on the theory that
* users could have multiple bots run from one process) rather than per-bot.
* Therefore, store these at file scope here rather than in the client's
* RESTManager object.
*/
let invalidCount = 0;
let invalidCountResetTime: number | null = null;
/**
* Increment the invalid request count and emit warning if necessary
*
* @internal
*/
export function incrementInvalidCount(manager: REST) {
if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
invalidCountResetTime = Date.now() + 1_000 * 60 * 10;
invalidCount = 0;
}
invalidCount++;
const emitInvalid =
manager.options.invalidRequestWarningInterval > 0 &&
invalidCount % manager.options.invalidRequestWarningInterval === 0;
if (emitInvalid) {
// Let library users know periodically about invalid requests
manager.emit(RESTEvents.InvalidRequestWarning, {
count: invalidCount,
remainingTime: invalidCountResetTime - Date.now(),
});
}
}
/**
* Performs the actual network request for a request handler
*
* @param manager - The manager that holds options and emits informational events
* @param routeId - The generalized api route with literal ids for major parameters
* @param url - The fully resolved url to make the request to
* @param options - The fetch options needed to make the request
* @param requestData - Extra data from the user's request needed for errors and additional processing
* @param retries - The number of retries this request has already attempted (recursion occurs on the handler)
* @returns The respond from the network or `null` when the request should be retried
* @internal
*/
export async function makeNetworkRequest(
manager: REST,
routeId: RouteData,
url: string,
options: RequestInit,
requestData: HandlerRequestData,
retries: number,
) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), manager.options.timeout);
if (requestData.signal) {
// If the user signal was aborted, abort the controller, else abort the local signal.
// The reason why we don't re-use the user's signal, is because users may use the same signal for multiple
// requests, and we do not want to cause unexpected side-effects.
if (requestData.signal.aborted) controller.abort();
else requestData.signal.addEventListener('abort', () => controller.abort());
}
let res: ResponseLike;
try {
res = await manager.options.makeRequest(url, { ...options, signal: controller.signal });
} catch (error: unknown) {
if (!(error instanceof Error)) throw error;
// Retry the specified number of times if needed
if (shouldRetry(error) && retries !== manager.options.retries) {
// Retry is handled by the handler upon receiving null
return null;
}
throw error;
} finally {
clearTimeout(timeout);
}
if (manager.listenerCount(RESTEvents.Response)) {
manager.emit(
RESTEvents.Response,
{
method: options.method ?? 'get',
path: routeId.original,
route: routeId.bucketRoute,
options,
data: requestData,
retries,
},
res instanceof Response ? res.clone() : { ...res },
);
}
return res;
}
/**
* Handles 5xx and 4xx errors (not 429's) conventionally. 429's should be handled before calling this function
*
* @param manager - The manager that holds options and emits informational events
* @param res - The response received from {@link makeNetworkRequest}
* @param method - The method used to make the request
* @param url - The fully resolved url to make the request to
* @param requestData - Extra data from the user's request needed for errors and additional processing
* @param retries - The number of retries this request has already attempted (recursion occurs on the handler)
* @returns The response if the status code is not handled or null to request a retry
*/
export async function handleErrors(
manager: REST,
res: ResponseLike,
method: string,
url: string,
requestData: HandlerRequestData,
retries: number,
) {
const status = res.status;
if (status >= 500 && status < 600) {
// Retry the specified number of times for possible server side issues
if (retries !== manager.options.retries) {
return null;
}
// We are out of retries, throw an error
throw new HTTPError(status, res.statusText, method, url, requestData);
} else {
// Handle possible malformed requests
if (status >= 400 && status < 500) {
// If we receive this status code, it means the token we had is no longer valid.
if (status === 401 && requestData.auth) {
manager.setToken(null!);
}
// The request will not succeed for some reason, parse the error returned from the api
const data = (await parseResponse(res)) as DiscordErrorData | OAuthErrorData;
// throw the API error
throw new DiscordAPIError(data, 'code' in data ? data.code : data.error, status, method, url, requestData);
}
return res;
}
}