diff --git a/package-lock.json b/package-lock.json
index 77fa56fee9b4..87a18f9201d1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@discordjs/collection": "^0.2.1",
"@discordjs/form-data": "^3.0.1",
"@sapphire/async-queue": "^1.1.5",
+ "@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.0",
"discord-api-types": "^0.23.1",
"node-fetch": "^2.6.1",
@@ -2307,6 +2308,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
"integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ=="
},
+ "node_modules/@types/node-fetch": {
+ "version": "2.5.12",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
+ "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^3.0.0"
+ }
+ },
"node_modules/@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -5636,7 +5646,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
- "dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -14018,6 +14027,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz",
"integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ=="
},
+ "@types/node-fetch": {
+ "version": "2.5.12",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz",
+ "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==",
+ "requires": {
+ "@types/node": "*",
+ "form-data": "^3.0.0"
+ }
+ },
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@@ -16627,7 +16645,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
- "dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
diff --git a/package.json b/package.json
index dae2659dc591..f35cdfa5194f 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"@discordjs/collection": "^0.2.1",
"@discordjs/form-data": "^3.0.1",
"@sapphire/async-queue": "^1.1.5",
+ "@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.0",
"discord-api-types": "^0.23.1",
"node-fetch": "^2.6.1",
diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js
index d80570dac224..6ad8fdfa703a 100644
--- a/src/rest/RequestHandler.js
+++ b/src/rest/RequestHandler.js
@@ -5,7 +5,7 @@ const DiscordAPIError = require('./DiscordAPIError');
const HTTPError = require('./HTTPError');
const RateLimitError = require('./RateLimitError');
const {
- Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING },
+ Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST },
} = require('../util/Constants');
const Util = require('../util/Util');
@@ -162,6 +162,34 @@ class RequestHandler {
}
this.manager.globalRemaining--;
+ /**
+ * Represents a request that will or has been made to the Discord API
+ * @typedef {Object} APIRequest
+ * @property {HTTPMethod} method The HTTP method used in this request
+ * @property {string} path The full path used to make the request
+ * @property {string} route The API route identifying the ratelimit for this request
+ * @property {Object} options Additional options for this request
+ * @property {number} retries The number of times this request has been attempted
+ */
+
+ if (this.manager.client.listenerCount(API_REQUEST)) {
+ /**
+ * Emitted before every API request.
+ * This event can emit several times for the same request, e.g. when hitting a rate limit.
+ * This is an informational event that is emitted quite frequently,
+ * it is highly recommended to check `request.path` to filter the data.
+ * @event Client#apiRequest
+ * @param {APIRequest} request The request that is about to be sent
+ */
+ this.manager.client.emit(API_REQUEST, {
+ method: request.method,
+ path: request.path,
+ route: request.route,
+ options: request.options,
+ retries: request.retries,
+ });
+ }
+
// Perform the request
let res;
try {
@@ -176,6 +204,29 @@ class RequestHandler {
return this.execute(request);
}
+ if (this.manager.client.listenerCount(API_RESPONSE)) {
+ /**
+ * Emitted after every API request has received a response.
+ * This event does not necessarily correlate to completion of the request, e.g. when hitting a rate limit.
+ * This is an informational event that is emitted quite frequently,
+ * it is highly recommended to check `request.path` to filter the data.
+ * @event Client#apiResponse
+ * @param {APIRequest} request The request that triggered this response
+ * @param {Response} response The response received from the Discord API
+ */
+ this.manager.client.emit(
+ API_RESPONSE,
+ {
+ method: request.method,
+ path: request.path,
+ route: request.route,
+ options: request.options,
+ retries: request.retries,
+ },
+ res.clone(),
+ );
+ }
+
let sublimitTimeout;
if (res.headers) {
const serverDate = res.headers.get('date');
@@ -315,3 +366,13 @@ class RequestHandler {
}
module.exports = RequestHandler;
+
+/**
+ * @external HTTPMethod
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods}
+ */
+
+/**
+ * @external Response
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response}
+ */
diff --git a/src/util/Constants.js b/src/util/Constants.js
index b6e488b796e9..82c8c5fa3765 100644
--- a/src/util/Constants.js
+++ b/src/util/Constants.js
@@ -124,6 +124,8 @@ exports.Opcodes = {
exports.Events = {
RATE_LIMIT: 'rateLimit',
INVALID_REQUEST_WARNING: 'invalidRequestWarning',
+ API_RESPONSE: 'apiResponse',
+ API_REQUEST: 'apiRequest',
CLIENT_READY: 'ready',
APPLICATION_COMMAND_CREATE: 'applicationCommandCreate',
APPLICATION_COMMAND_DELETE: 'applicationCommandDelete',
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 7d8c60a0ae76..4816402f9216 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -49,6 +49,7 @@ import {
import { ChildProcess } from 'node:child_process';
import { EventEmitter } from 'node:events';
import { AgentOptions } from 'node:https';
+import { Response } from 'node-fetch';
import { Stream } from 'node:stream';
import { MessagePort, Worker } from 'node:worker_threads';
import * as WebSocket from 'ws';
@@ -3129,6 +3130,14 @@ export interface APIErrors {
STICKER_ANIMATION_DURATION_EXCEEDS_MAXIMUM_OF_5_SECONDS: 170007;
}
+export interface APIRequest {
+ method: 'get' | 'post' | 'delete' | 'patch' | 'put';
+ options: unknown;
+ path: string;
+ retries: number;
+ route: string;
+}
+
export interface ApplicationAsset {
name: string;
id: Snowflake;
@@ -3439,6 +3448,8 @@ export interface ChannelWebhookCreateOptions {
}
export interface ClientEvents {
+ apiResponse: [request: APIRequest, response: Response];
+ apiRequest: [request: APIRequest];
applicationCommandCreate: [command: ApplicationCommand];
applicationCommandDelete: [command: ApplicationCommand];
applicationCommandUpdate: [oldCommand: ApplicationCommand | null, newCommand: ApplicationCommand];
@@ -3677,6 +3688,8 @@ export interface ConstantsColors {
export interface ConstantsEvents {
RATE_LIMIT: 'rateLimit';
INVALID_REQUEST_WARNING: 'invalidRequestWarning';
+ API_RESPONSE: 'apiResponse';
+ API_REQUEST: 'apiRequest';
CLIENT_READY: 'ready';
APPLICATION_COMMAND_CREATE: 'applicationCommandCreate';
APPLICATION_COMMAND_DELETE: 'applicationCommandDelete';