mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-18 12:33:30 +01:00
refactor(rest): switch api to fetch-like and provide strategies (#9416)
BREAKING CHANGE: NodeJS v18+ is required when using node due to the use of global `fetch` BREAKING CHANGE: The raw method of REST now returns a web compatible `Respone` object. BREAKING CHANGE: The `parseResponse` utility method has been updated to operate on a web compatible `Response` object. BREAKING CHANGE: Many underlying internals have changed, some of which were exported. BREAKING CHANGE: `DefaultRestOptions` used to contain a default `agent`, which is now set to `null` instead.
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import type { Dispatcher } from 'undici';
|
||||
import type { RequestOptions } from '../REST.js';
|
||||
import type { RequestInit } from 'undici';
|
||||
import type { ResponseLike } from '../REST.js';
|
||||
import type { HandlerRequestData, RequestManager, RouteData } from '../RequestManager.js';
|
||||
import type { IHandler } from '../interfaces/Handler.js';
|
||||
import { RESTEvents } from '../utils/constants.js';
|
||||
import { onRateLimit, parseHeader } from '../utils/utils.js';
|
||||
import type { IHandler } from './IHandler.js';
|
||||
import { onRateLimit } from '../utils/utils.js';
|
||||
import { handleErrors, incrementInvalidCount, makeNetworkRequest } from './Shared.js';
|
||||
|
||||
/**
|
||||
@@ -54,9 +54,9 @@ export class BurstHandler implements IHandler {
|
||||
public async queueRequest(
|
||||
routeId: RouteData,
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
options: RequestInit,
|
||||
requestData: HandlerRequestData,
|
||||
): Promise<Dispatcher.ResponseData> {
|
||||
): Promise<ResponseLike> {
|
||||
return this.runRequest(routeId, url, options, requestData);
|
||||
}
|
||||
|
||||
@@ -72,10 +72,10 @@ export class BurstHandler implements IHandler {
|
||||
private async runRequest(
|
||||
routeId: RouteData,
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
options: RequestInit,
|
||||
requestData: HandlerRequestData,
|
||||
retries = 0,
|
||||
): Promise<Dispatcher.ResponseData> {
|
||||
): Promise<ResponseLike> {
|
||||
const method = options.method ?? 'get';
|
||||
|
||||
const res = await makeNetworkRequest(this.manager, routeId, url, options, requestData, retries);
|
||||
@@ -86,9 +86,9 @@ export class BurstHandler implements IHandler {
|
||||
return this.runRequest(routeId, url, options, requestData, ++retries);
|
||||
}
|
||||
|
||||
const status = res.statusCode;
|
||||
const status = res.status;
|
||||
let retryAfter = 0;
|
||||
const retry = parseHeader(res.headers['retry-after']);
|
||||
const retry = res.headers.get('Retry-After');
|
||||
|
||||
// Amount of time in milliseconds until we should retry if rate limited (globally or otherwise)
|
||||
if (retry) retryAfter = Number(retry) * 1_000 + this.manager.options.offset;
|
||||
@@ -102,7 +102,7 @@ export class BurstHandler implements IHandler {
|
||||
return res;
|
||||
} else if (status === 429) {
|
||||
// Unexpected ratelimit
|
||||
const isGlobal = res.headers['x-ratelimit-global'] !== undefined;
|
||||
const isGlobal = res.headers.has('X-RateLimit-Global');
|
||||
await onRateLimit(this.manager, {
|
||||
timeToReset: retryAfter,
|
||||
limit: Number.POSITIVE_INFINITY,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Dispatcher } from 'undici';
|
||||
import type { RequestOptions } from '../REST.js';
|
||||
import type { HandlerRequestData, RouteData } from '../RequestManager.js';
|
||||
|
||||
export interface IHandler {
|
||||
/**
|
||||
* The unique id of the handler
|
||||
*/
|
||||
readonly id: string;
|
||||
/**
|
||||
* If the bucket is currently inactive (no pending requests)
|
||||
*/
|
||||
get inactive(): boolean;
|
||||
/**
|
||||
* Queues a request to be sent
|
||||
*
|
||||
* @param routeId - The generalized api route with literal ids for major parameters
|
||||
* @param url - The url to do the request on
|
||||
* @param options - All the information needed to make a request
|
||||
* @param requestData - Extra data from the user's request needed for errors and additional processing
|
||||
*/
|
||||
queueRequest(
|
||||
routeId: RouteData,
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
requestData: HandlerRequestData,
|
||||
): Promise<Dispatcher.ResponseData>;
|
||||
}
|
||||
|
||||
export interface PolyFillAbortSignal {
|
||||
readonly aborted: boolean;
|
||||
addEventListener(type: 'abort', listener: () => void): void;
|
||||
removeEventListener(type: 'abort', listener: () => void): void;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
import { AsyncQueue } from '@sapphire/async-queue';
|
||||
import type { Dispatcher } from 'undici';
|
||||
import type { RateLimitData, RequestOptions } from '../REST.js';
|
||||
import type { RequestInit } from 'undici';
|
||||
import type { RateLimitData, ResponseLike } from '../REST.js';
|
||||
import type { HandlerRequestData, RequestManager, RouteData } from '../RequestManager.js';
|
||||
import type { IHandler } from '../interfaces/Handler.js';
|
||||
import { RESTEvents } from '../utils/constants.js';
|
||||
import { hasSublimit, onRateLimit, parseHeader } from '../utils/utils.js';
|
||||
import type { IHandler } from './IHandler.js';
|
||||
import { hasSublimit, onRateLimit } from '../utils/utils.js';
|
||||
import { handleErrors, incrementInvalidCount, makeNetworkRequest } from './Shared.js';
|
||||
|
||||
const enum QueueType {
|
||||
@@ -134,9 +134,9 @@ export class SequentialHandler implements IHandler {
|
||||
public async queueRequest(
|
||||
routeId: RouteData,
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
options: RequestInit,
|
||||
requestData: HandlerRequestData,
|
||||
): Promise<Dispatcher.ResponseData> {
|
||||
): Promise<ResponseLike> {
|
||||
let queue = this.#asyncQueue;
|
||||
let queueType = QueueType.Standard;
|
||||
// Separate sublimited requests when already sublimited
|
||||
@@ -195,10 +195,10 @@ export class SequentialHandler implements IHandler {
|
||||
private async runRequest(
|
||||
routeId: RouteData,
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
options: RequestInit,
|
||||
requestData: HandlerRequestData,
|
||||
retries = 0,
|
||||
): Promise<Dispatcher.ResponseData> {
|
||||
): Promise<ResponseLike> {
|
||||
/*
|
||||
* After calculations have been done, pre-emptively stop further requests
|
||||
* Potentially loop until this task can run if e.g. the global rate limit is hit twice
|
||||
@@ -270,14 +270,14 @@ export class SequentialHandler implements IHandler {
|
||||
return this.runRequest(routeId, url, options, requestData, ++retries);
|
||||
}
|
||||
|
||||
const status = res.statusCode;
|
||||
const status = res.status;
|
||||
let retryAfter = 0;
|
||||
|
||||
const limit = parseHeader(res.headers['x-ratelimit-limit']);
|
||||
const remaining = parseHeader(res.headers['x-ratelimit-remaining']);
|
||||
const reset = parseHeader(res.headers['x-ratelimit-reset-after']);
|
||||
const hash = parseHeader(res.headers['x-ratelimit-bucket']);
|
||||
const retry = parseHeader(res.headers['retry-after']);
|
||||
const limit = res.headers.get('X-RateLimit-Limit');
|
||||
const remaining = res.headers.get('X-RateLimit-Remaining');
|
||||
const reset = res.headers.get('X-RateLimit-Reset-After');
|
||||
const hash = res.headers.get('X-RateLimit-Bucket');
|
||||
const retry = res.headers.get('Retry-After');
|
||||
|
||||
// Update the total number of requests that can be made before the rate limit resets
|
||||
this.limit = limit ? Number(limit) : Number.POSITIVE_INFINITY;
|
||||
@@ -309,7 +309,7 @@ export class SequentialHandler implements IHandler {
|
||||
// Handle retryAfter, which means we have actually hit a rate limit
|
||||
let sublimitTimeout: number | null = null;
|
||||
if (retryAfter > 0) {
|
||||
if (res.headers['x-ratelimit-global'] !== undefined) {
|
||||
if (res.headers.has('X-RateLimit-Global')) {
|
||||
this.manager.globalRemaining = 0;
|
||||
this.manager.globalReset = Date.now() + retryAfter;
|
||||
} else if (!this.localLimited) {
|
||||
@@ -327,7 +327,7 @@ export class SequentialHandler implements IHandler {
|
||||
incrementInvalidCount(this.manager);
|
||||
}
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
if (res.ok) {
|
||||
return res;
|
||||
} else if (status === 429) {
|
||||
// A rate limit was hit - this may happen if the route isn't associated with an official bucket hash yet, or when first globally rate limited
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { setTimeout, clearTimeout } from 'node:timers';
|
||||
import { request, type Dispatcher } from 'undici';
|
||||
import type { RequestOptions } from '../REST.js';
|
||||
import { Response } from 'undici';
|
||||
import type { RequestInit } from 'undici';
|
||||
import type { ResponseLike } from '../REST.js';
|
||||
import type { HandlerRequestData, RequestManager, RouteData } from '../RequestManager.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 { parseResponse, shouldRetry } from '../utils/utils.js';
|
||||
import type { PolyFillAbortSignal } from './IHandler.js';
|
||||
|
||||
/**
|
||||
* Invalid request limiting is done on a per-IP basis, not a per-token basis.
|
||||
@@ -60,25 +60,23 @@ export async function makeNetworkRequest(
|
||||
manager: RequestManager,
|
||||
routeId: RouteData,
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
options: RequestInit,
|
||||
requestData: HandlerRequestData,
|
||||
retries: number,
|
||||
) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), manager.options.timeout).unref();
|
||||
if (requestData.signal) {
|
||||
// The type polyfill is required because Node.js's types are incomplete.
|
||||
const signal = requestData.signal as unknown as PolyFillAbortSignal;
|
||||
// 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 (signal.aborted) controller.abort();
|
||||
else signal.addEventListener('abort', () => controller.abort());
|
||||
if (requestData.signal.aborted) controller.abort();
|
||||
else requestData.signal.addEventListener('abort', () => controller.abort());
|
||||
}
|
||||
|
||||
let res: Dispatcher.ResponseData;
|
||||
let res: ResponseLike;
|
||||
try {
|
||||
res = await request(url, { ...options, signal: controller.signal });
|
||||
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
|
||||
@@ -103,7 +101,7 @@ export async function makeNetworkRequest(
|
||||
data: requestData,
|
||||
retries,
|
||||
},
|
||||
{ ...res },
|
||||
res instanceof Response ? res.clone() : { ...res },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,13 +121,13 @@ export async function makeNetworkRequest(
|
||||
*/
|
||||
export async function handleErrors(
|
||||
manager: RequestManager,
|
||||
res: Dispatcher.ResponseData,
|
||||
res: ResponseLike,
|
||||
method: string,
|
||||
url: string,
|
||||
requestData: HandlerRequestData,
|
||||
retries: number,
|
||||
) {
|
||||
const status = res.statusCode;
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user