diff --git a/packages/rest/__tests__/DiscordAPIError.test.ts b/packages/rest/__tests__/DiscordAPIError.test.ts index f297820..07da9ec 100644 --- a/packages/rest/__tests__/DiscordAPIError.test.ts +++ b/packages/rest/__tests__/DiscordAPIError.test.ts @@ -7,6 +7,10 @@ test('Unauthorized', () => { 401, 'PATCH', 'https://discord.com/api/v9/guilds/:id', + { + attachments: undefined, + body: undefined, + }, ); expect(error.code).toBe(0); @@ -15,6 +19,8 @@ test('Unauthorized', () => { expect(error.name).toBe('DiscordAPIError[0]'); expect(error.status).toBe(401); expect(error.url).toBe('https://discord.com/api/v9/guilds/:id'); + expect(error.requestBody.attachments).toBe(undefined); + expect(error.requestBody.json).toBe(undefined); }); test('Invalid Form Body Error (error.{property}._errors.{index})', () => { @@ -30,6 +36,12 @@ test('Invalid Form Body Error (error.{property}._errors.{index})', () => { 400, 'PATCH', 'https://discord.com/api/v9/users/@me', + { + attachments: undefined, + body: { + username: 'a', + }, + }, ); expect(error.code).toBe(50035); @@ -40,6 +52,8 @@ test('Invalid Form Body Error (error.{property}._errors.{index})', () => { expect(error.name).toBe('DiscordAPIError[50035]'); expect(error.status).toBe(400); expect(error.url).toBe('https://discord.com/api/v9/users/@me'); + expect(error.requestBody.attachments).toBe(undefined); + expect(error.requestBody.json).toStrictEqual({ username: 'a' }); }); test('Invalid FormFields Error (error.errors.{property}.{property}.{index}.{property}._errors.{index})', () => { @@ -57,6 +71,7 @@ test('Invalid FormFields Error (error.errors.{property}.{property}.{index}.{prop 400, 'POST', 'https://discord.com/api/v9/channels/:id', + {}, ); expect(error.code).toBe(50035); @@ -84,6 +99,7 @@ test('Invalid FormFields Error (error.errors.{property}.{property}._errors.{inde 400, 'PATCH', 'https://discord.com/api/v9/guilds/:id', + {}, ); expect(error.code).toBe(50035); diff --git a/packages/rest/src/lib/RequestManager.ts b/packages/rest/src/lib/RequestManager.ts index 4444ab3..8c60322 100644 --- a/packages/rest/src/lib/RequestManager.ts +++ b/packages/rest/src/lib/RequestManager.ts @@ -163,7 +163,7 @@ export class RequestManager extends EventEmitter { const { url, fetchOptions } = this.resolveRequest(request); // Queue the request - return handler.queueRequest(routeId, url, fetchOptions); + return handler.queueRequest(routeId, url, fetchOptions, { body: request.body, attachments: request.attachments }); } /** diff --git a/packages/rest/src/lib/errors/DiscordAPIError.ts b/packages/rest/src/lib/errors/DiscordAPIError.ts index 7d0ab6a..4a8487e 100644 --- a/packages/rest/src/lib/errors/DiscordAPIError.ts +++ b/packages/rest/src/lib/errors/DiscordAPIError.ts @@ -1,3 +1,5 @@ +import type { InternalRequest, RawAttachment } from '../RequestManager'; + interface DiscordErrorFieldInformation { code: string; message: string; @@ -15,6 +17,11 @@ export interface DiscordErrorData { errors?: DiscordError; } +export interface RequestBody { + attachments: RawAttachment[] | undefined; + json: unknown | undefined; +} + function isErrorGroupWrapper(error: any): error is DiscordErrorGroupWrapper { return Reflect.has(error, '_errors'); } @@ -28,12 +35,15 @@ function isErrorResponse(error: any): error is DiscordErrorFieldInformation { * @extends Error */ export class DiscordAPIError extends Error { + public requestBody: RequestBody; + /** * @param rawError The error reported by Discord * @param code The error code reported by Discord * @param status The status code of the response * @param method The method of the request that erred * @param url The url of the request that erred + * @param bodyData The unparsed data for the request that errored */ public constructor( public rawError: DiscordErrorData, @@ -41,8 +51,11 @@ export class DiscordAPIError extends Error { public status: number, public method: string, public url: string, + bodyData: Pick, ) { super(DiscordAPIError.getMessage(rawError)); + + this.requestBody = { attachments: bodyData.attachments, json: bodyData.body }; } /** diff --git a/packages/rest/src/lib/errors/HTTPError.ts b/packages/rest/src/lib/errors/HTTPError.ts index 578e908..825d012 100644 --- a/packages/rest/src/lib/errors/HTTPError.ts +++ b/packages/rest/src/lib/errors/HTTPError.ts @@ -1,13 +1,19 @@ +import type { InternalRequest } from '../RequestManager'; +import type { RequestBody } from './DiscordAPIError'; + /** * Represents a HTTP error */ export class HTTPError extends Error { + public requestBody: RequestBody; + /** * @param message The error message * @param name The name of the error * @param status The status code of the response * @param method The method of the request that erred * @param url The url of the request that erred + * @param bodyData The unparsed data for the request that errored */ public constructor( message: string, @@ -15,7 +21,10 @@ export class HTTPError extends Error { public status: number, public method: string, public url: string, + bodyData: Pick, ) { super(message); + + this.requestBody = { attachments: bodyData.attachments, json: bodyData.body }; } } diff --git a/packages/rest/src/lib/handlers/IHandler.ts b/packages/rest/src/lib/handlers/IHandler.ts index 89a0deb..cd04871 100644 --- a/packages/rest/src/lib/handlers/IHandler.ts +++ b/packages/rest/src/lib/handlers/IHandler.ts @@ -1,6 +1,11 @@ import type { RequestInit } from 'node-fetch'; -import type { RouteData } from '../RequestManager'; +import type { InternalRequest, RouteData } from '../RequestManager'; export interface IHandler { - queueRequest(routeId: RouteData, url: string, options: RequestInit): Promise; + queueRequest( + routeId: RouteData, + url: string, + options: RequestInit, + bodyData: Pick, + ): Promise; } diff --git a/packages/rest/src/lib/handlers/SequentialHandler.ts b/packages/rest/src/lib/handlers/SequentialHandler.ts index bffdf69..267985c 100644 --- a/packages/rest/src/lib/handlers/SequentialHandler.ts +++ b/packages/rest/src/lib/handlers/SequentialHandler.ts @@ -3,7 +3,7 @@ import { AsyncQueue } from '@sapphire/async-queue'; import fetch, { RequestInit, Response } from 'node-fetch'; import { DiscordAPIError, DiscordErrorData } from '../errors/DiscordAPIError'; import { HTTPError } from '../errors/HTTPError'; -import type { RequestManager, RouteData } from '../RequestManager'; +import type { InternalRequest, RequestManager, RouteData } from '../RequestManager'; import { RESTEvents } from '../utils/constants'; import { parseResponse } from '../utils/utils'; @@ -84,8 +84,14 @@ export class SequentialHandler { * @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 bodyData The data taht was used to form the body, passed to any errors generated */ - public async queueRequest(routeId: RouteData, url: string, options: RequestInit): Promise { + public async queueRequest( + routeId: RouteData, + url: string, + options: RequestInit, + bodyData: Pick, + ): Promise { // Wait for any previous requests to be completed before this one is run await this.#asyncQueue.wait(); try { @@ -107,7 +113,7 @@ export class SequentialHandler { await sleep(this.timeToReset); } // Make the request, and return the results - return await this.runRequest(routeId, url, options); + return await this.runRequest(routeId, url, options, bodyData); } finally { // Allow the next request to fire this.#asyncQueue.shift(); @@ -119,9 +125,16 @@ export class SequentialHandler { * @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 node-fetch options needed to make the request + * @param bodyData The data that was used to form the body, passed to any errors generated * @param retries The number of retries this request has already attempted (recursion) */ - private async runRequest(routeId: RouteData, url: string, options: RequestInit, retries = 0): Promise { + private async runRequest( + routeId: RouteData, + url: string, + options: RequestInit, + bodyData: Pick, + retries = 0, + ): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout); let res: Response; @@ -131,7 +144,7 @@ export class SequentialHandler { } catch (error: unknown) { // Retry the specified number of times for possible timed out requests if (error instanceof Error && error.name === 'AbortError' && retries !== this.manager.options.retries) { - return this.runRequest(routeId, url, options, ++retries); + return this.runRequest(routeId, url, options, bodyData, ++retries); } throw error; @@ -192,14 +205,14 @@ export class SequentialHandler { // Wait the retryAfter amount of time before retrying the request await sleep(retryAfter); // Since this is not a server side issue, the next request should pass, so we don't bump the retries counter - return this.runRequest(routeId, url, options, retries); + return this.runRequest(routeId, url, options, bodyData, retries); } else if (res.status >= 500 && res.status < 600) { // Retry the specified number of times for possible server side issues if (retries !== this.manager.options.retries) { - return this.runRequest(routeId, url, options, ++retries); + return this.runRequest(routeId, url, options, bodyData, ++retries); } // We are out of retries, throw an error - throw new HTTPError(res.statusText, res.constructor.name, res.status, method, url); + throw new HTTPError(res.statusText, res.constructor.name, res.status, method, url, bodyData); } else { // Handle possible malformed requests if (res.status >= 400 && res.status < 500) { @@ -210,7 +223,7 @@ export class SequentialHandler { // The request will not succeed for some reason, parse the error returned from the api const data = (await parseResponse(res)) as DiscordErrorData; // throw the API error - throw new DiscordAPIError(data, data.code, res.status, method, url); + throw new DiscordAPIError(data, data.code, res.status, method, url, bodyData); } return null; }