Skip to content
This repository has been archived by the owner on Jan 8, 2022. It is now read-only.

feat(Errors): show data sent when an error occurs #72

Merged
merged 2 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/rest/__tests__/DiscordAPIError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ test('Unauthorized', () => {
401,
'PATCH',
'https://discord.com/api/v9/guilds/:id',
{
attachments: undefined,
body: undefined,
},
);

expect(error.code).toBe(0);
Expand All @@ -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})', () => {
Expand All @@ -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);
Expand All @@ -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})', () => {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/src/lib/RequestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/rest/src/lib/errors/DiscordAPIError.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { InternalRequest, RawAttachment } from '../RequestManager';

interface DiscordErrorFieldInformation {
code: string;
message: string;
Expand All @@ -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');
}
Expand All @@ -28,21 +35,27 @@ 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,
public code: number,
public status: number,
public method: string,
public url: string,
bodyData: Pick<InternalRequest, 'attachments' | 'body'>,
) {
super(DiscordAPIError.getMessage(rawError));

this.requestBody = { attachments: bodyData.attachments, json: bodyData.body };
}

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/rest/src/lib/errors/HTTPError.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
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,
public name: string,
public status: number,
public method: string,
public url: string,
bodyData: Pick<InternalRequest, 'attachments' | 'body'>,
) {
super(message);

this.requestBody = { attachments: bodyData.attachments, json: bodyData.body };
}
}
9 changes: 7 additions & 2 deletions packages/rest/src/lib/handlers/IHandler.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>;
queueRequest(
routeId: RouteData,
url: string,
options: RequestInit,
bodyData: Pick<InternalRequest, 'attachments' | 'body'>,
): Promise<unknown>;
}
31 changes: 22 additions & 9 deletions packages/rest/src/lib/handlers/SequentialHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AbortController from 'abort-controller';
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';

Expand Down Expand Up @@ -85,8 +85,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<unknown> {
public async queueRequest(
routeId: RouteData,
url: string,
options: RequestInit,
bodyData: Pick<InternalRequest, 'attachments' | 'body'>,
): Promise<unknown> {
// Wait for any previous requests to be completed before this one is run
await this.#asyncQueue.wait();
try {
Expand All @@ -108,7 +114,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();
Expand All @@ -120,9 +126,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<unknown> {
private async runRequest(
routeId: RouteData,
url: string,
options: RequestInit,
bodyData: Pick<InternalRequest, 'attachments' | 'body'>,
retries = 0,
): Promise<unknown> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.manager.options.timeout);
let res: Response;
Expand All @@ -132,7 +145,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;
Expand Down Expand Up @@ -193,14 +206,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) {
Expand All @@ -211,7 +224,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;
}
Expand Down