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

Commit

Permalink
feat(Errors): show data sent when an error occurs (#72)
Browse files Browse the repository at this point in the history
Co-authored-by: Vlad Frangu <kingdgrizzle@gmail.com>
  • Loading branch information
ckohen and vladfrangu authored Oct 18, 2021
1 parent 3b53910 commit 3e2edc8
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 12 deletions.
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 @@ -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';

Expand Down Expand Up @@ -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<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 @@ -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();
Expand All @@ -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<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 @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down

0 comments on commit 3e2edc8

Please sign in to comment.